mirror of
https://github.com/pockethost/pockethost.git
synced 2025-07-04 03:42:29 +00:00
enh: FTP support
This commit is contained in:
parent
a1116d3faf
commit
29f4e5c477
@ -16,31 +16,20 @@ export type FtpConfig = SingletonBaseConfig & {}
|
||||
|
||||
export enum FolderNames {
|
||||
PbData = 'pb_data',
|
||||
PbStatic = 'pb_static',
|
||||
// PbStatic = 'pb_static',
|
||||
PbMigrations = 'pb_migrations',
|
||||
PbWorker = 'worker',
|
||||
PbBackup = 'backup',
|
||||
}
|
||||
|
||||
export const README_CONTENTS: { [_ in FolderNames]: string } = {
|
||||
[FolderNames.PbBackup]: `This directory contains tgz backups of your data. PocketHost creates these automatically, or you can create them manually. For more information, see https://pockethost.io/docs/backups`,
|
||||
[FolderNames.PbData]: `This directory contains your PocketBase data. For more information, see https://pockethost.io/docs/data`,
|
||||
[FolderNames.PbStatic]: `This directory contains static files such as your web frontend. PocketHost will serve these when your instance URL receives a request. For more information, see https://pockethost.io/docs/static `,
|
||||
[FolderNames.PbWorker]: `This directory contains your Deno worker. For more information, see https://pockethost.io/docs/workers`,
|
||||
[FolderNames.PbMigrations]: `This directory contains your migrations. For more information, see https://pockethost.io/docs/migrations`,
|
||||
}
|
||||
export const README_NAME = 'readme.md'
|
||||
|
||||
export const FOLDER_NAMES: FolderNames[] = [
|
||||
FolderNames.PbBackup,
|
||||
export const INSTANCE_ROOT_FOLDER_NAMES: FolderNames[] = [
|
||||
FolderNames.PbData,
|
||||
FolderNames.PbStatic,
|
||||
// FolderNames.PbStatic,
|
||||
FolderNames.PbWorker,
|
||||
FolderNames.PbMigrations,
|
||||
]
|
||||
|
||||
export function isFolder(name: string): name is FolderNames {
|
||||
return FOLDER_NAMES.includes(name as FolderNames)
|
||||
export function isInstanceRootFolder(name: string): name is FolderNames {
|
||||
return INSTANCE_ROOT_FOLDER_NAMES.includes(name as FolderNames)
|
||||
}
|
||||
|
||||
const tls = {
|
||||
|
@ -1,59 +1,180 @@
|
||||
import { DAEMON_PB_DATA_DIR } from '$constants'
|
||||
import { Logger } from '@pockethost/common'
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
|
||||
import { join } from 'path'
|
||||
import { Readable } from 'stream'
|
||||
import { PocketbaseClientApi } from '../clientService/PbClient'
|
||||
import { compact, map } from '@s-libs/micro-dash'
|
||||
import {
|
||||
FOLDER_NAMES,
|
||||
isFolder,
|
||||
README_CONTENTS,
|
||||
README_NAME,
|
||||
} from './FtpService'
|
||||
constants,
|
||||
createReadStream,
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
Mode,
|
||||
} from 'fs'
|
||||
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { isAbsolute, join, normalize, resolve, sep } from 'path'
|
||||
import { PocketbaseClientApi } from '../clientService/PbClient'
|
||||
import * as fsAsync from './fs-async'
|
||||
import { INSTANCE_ROOT_FOLDER_NAMES, isInstanceRootFolder } from './FtpService'
|
||||
|
||||
export class PhFs extends FileSystem {
|
||||
const nanoid = customAlphabet(`abcdefghijklmnop`)
|
||||
|
||||
export type PathError = {
|
||||
cause: {
|
||||
errno: number
|
||||
code: string
|
||||
syscall: string
|
||||
path: string
|
||||
}
|
||||
isOperational: boolean
|
||||
errno: number
|
||||
code: string
|
||||
syscall: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const UNIX_SEP_REGEX = /\//g
|
||||
const WIN_SEP_REGEX = /\\/g
|
||||
|
||||
export class PhFs implements FileSystem {
|
||||
private log: Logger
|
||||
private client: PocketbaseClientApi
|
||||
connection: FtpConnection
|
||||
cwd: string
|
||||
private _root: string
|
||||
|
||||
constructor(
|
||||
connection: FtpConnection,
|
||||
client: PocketbaseClientApi,
|
||||
logger: Logger
|
||||
) {
|
||||
super(connection, { root: '/', cwd: '/' })
|
||||
const cwd = `/`
|
||||
const root = DAEMON_PB_DATA_DIR
|
||||
this.connection = connection
|
||||
this.client = client
|
||||
this.log = logger.create(`PhFs`)
|
||||
this.cwd = normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'))
|
||||
this._root = resolve(root || process.cwd())
|
||||
}
|
||||
|
||||
async chdir(path?: string | undefined): Promise<string> {
|
||||
this.log.dbg(`chdir`, path)
|
||||
if (!path) {
|
||||
throw new Error(`Expected path`)
|
||||
}
|
||||
const _path = path.startsWith('/') ? path : join(this.cwd, path)
|
||||
const [empty, subdomain] = _path.split('/')
|
||||
this.log.dbg({ _path, subdomain })
|
||||
get root() {
|
||||
return this._root
|
||||
}
|
||||
|
||||
if (subdomain) {
|
||||
const instance = await this.client.getInstanceBySubdomain(subdomain)
|
||||
if (!instance) {
|
||||
throw new Error(`Subdomain not found`)
|
||||
async _resolvePath(path = '.') {
|
||||
const { dbg } = this.log.create(`_resolvePath`)
|
||||
|
||||
// Unix separators normalize nicer on both unix and win platforms
|
||||
const resolvedPath = path.replace(WIN_SEP_REGEX, '/')
|
||||
|
||||
// Join cwd with new path
|
||||
const joinedPath = isAbsolute(resolvedPath)
|
||||
? normalize(resolvedPath)
|
||||
: join('/', this.cwd, resolvedPath)
|
||||
|
||||
// Create local filesystem path using the platform separator
|
||||
const [empty, subdomain, rootFolderName, ...pathFromRootFolder] =
|
||||
joinedPath.split('/')
|
||||
dbg({
|
||||
joinedPath,
|
||||
subdomain,
|
||||
rootFolderName,
|
||||
restOfPath: pathFromRootFolder,
|
||||
})
|
||||
|
||||
// Check if the root folder name is valid
|
||||
if (rootFolderName) {
|
||||
if (!isInstanceRootFolder(rootFolderName)) {
|
||||
throw new Error(`Accessing ${rootFolderName} is not allowed.`)
|
||||
}
|
||||
}
|
||||
this.cwd = _path
|
||||
return path
|
||||
|
||||
// Begin building the physical path
|
||||
const fsPathParts: string[] = [this._root]
|
||||
|
||||
// Check if the instance is valid
|
||||
const instance = await (async () => {
|
||||
if (subdomain) {
|
||||
const [instance] = await this.client.getInstanceBySubdomain(subdomain)
|
||||
if (!instance) {
|
||||
throw new Error(`${subdomain} not found.`)
|
||||
}
|
||||
fsPathParts.push(instance.id)
|
||||
if (rootFolderName) {
|
||||
fsPathParts.push(rootFolderName)
|
||||
// Ensure folder exists
|
||||
const rootFolderFsPath = resolve(
|
||||
join(...fsPathParts)
|
||||
.replace(UNIX_SEP_REGEX, sep)
|
||||
.replace(WIN_SEP_REGEX, sep)
|
||||
)
|
||||
if (!existsSync(rootFolderFsPath)) {
|
||||
mkdirSync(rootFolderFsPath)
|
||||
}
|
||||
}
|
||||
if (resolvedPath.length > 0) fsPathParts.push(...pathFromRootFolder)
|
||||
return instance
|
||||
}
|
||||
})()
|
||||
|
||||
// Finalize the fs path
|
||||
const fsPath = resolve(
|
||||
join(...fsPathParts)
|
||||
.replace(UNIX_SEP_REGEX, sep)
|
||||
.replace(WIN_SEP_REGEX, sep)
|
||||
)
|
||||
|
||||
// Create FTP client path using unix separator
|
||||
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/')
|
||||
|
||||
dbg({
|
||||
clientPath,
|
||||
fsPath,
|
||||
subdomain,
|
||||
rootFolderName,
|
||||
restOfPath: pathFromRootFolder,
|
||||
instance,
|
||||
})
|
||||
|
||||
return {
|
||||
clientPath,
|
||||
fsPath,
|
||||
subdomain,
|
||||
rootFolderName,
|
||||
pathFromRootFolder,
|
||||
instance,
|
||||
}
|
||||
}
|
||||
|
||||
async list(path?: string | undefined): Promise<FileStat[]> {
|
||||
this.log.dbg(`list ${path}`, this.cwd)
|
||||
if (!path) {
|
||||
throw new Error(`Expected path`)
|
||||
}
|
||||
const _path = path.startsWith('/') ? path : join(this.cwd, path)
|
||||
const [empty, subdomain, folderName, ...restOfPath] = _path.split('/')
|
||||
this.log.dbg({ _path, subdomain, folderName, restOfPath })
|
||||
currentDirectory() {
|
||||
return this.cwd
|
||||
}
|
||||
|
||||
async chdir(path = '.') {
|
||||
const { fsPath, clientPath } = await this._resolvePath(path)
|
||||
|
||||
return fsAsync
|
||||
.stat(fsPath)
|
||||
.then((stat) => {
|
||||
if (!stat.isDirectory()) throw new Error('Not a valid directory')
|
||||
})
|
||||
.then(() => {
|
||||
this.cwd = clientPath
|
||||
return this.currentDirectory()
|
||||
})
|
||||
}
|
||||
|
||||
async list(path = '.') {
|
||||
const { dbg, error } = this.log
|
||||
.create(`list`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(path)
|
||||
|
||||
const { fsPath, subdomain, rootFolderName, instance } =
|
||||
await this._resolvePath(path)
|
||||
|
||||
/*
|
||||
If a subdomain is not specified, we are in the user's root. List all subdomains.
|
||||
*/
|
||||
if (subdomain === '') {
|
||||
const instances = await this.client.getInstances()
|
||||
return instances.map((i) => {
|
||||
@ -66,15 +187,22 @@ export class PhFs extends FileSystem {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!subdomain) {
|
||||
throw new Error(`Subdomain expected in ${_path}`)
|
||||
}
|
||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
||||
|
||||
/*
|
||||
If we have a subdomain, then it should have resolved to an instance owned by that user
|
||||
*/
|
||||
if (!instance) {
|
||||
throw new Error(`Expected instance here`)
|
||||
throw new Error(
|
||||
`Something as gone wrong. An instance without a subdomain is not possible.`
|
||||
)
|
||||
}
|
||||
if (!folderName) {
|
||||
return FOLDER_NAMES.map((name) => ({
|
||||
|
||||
/*
|
||||
If there is no root folder name, then we are in the instance root. In this case, list
|
||||
our allowed folder names.
|
||||
*/
|
||||
if (!rootFolderName) {
|
||||
return INSTANCE_ROOT_FOLDER_NAMES.map((name) => ({
|
||||
isDirectory: () => true,
|
||||
mode: 0o755,
|
||||
size: 0,
|
||||
@ -82,33 +210,46 @@ export class PhFs extends FileSystem {
|
||||
name: name,
|
||||
}))
|
||||
}
|
||||
if (!isFolder(folderName)) {
|
||||
throw new Error(`Top level folder name ${folderName} not allowed.`)
|
||||
}
|
||||
const dir = join(DAEMON_PB_DATA_DIR, instance.id, folderName, ...restOfPath)
|
||||
this.log.dbg({ dir, exists: existsSync(dir) })
|
||||
return [
|
||||
...(restOfPath.length === 0
|
||||
? [
|
||||
{
|
||||
isDirectory: () => false,
|
||||
mode: 0o444,
|
||||
size: README_CONTENTS[folderName].length,
|
||||
mtime: Date.parse(instance.updated),
|
||||
name: README_NAME,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(existsSync(dir) ? await super.list(dir) : []),
|
||||
]
|
||||
|
||||
/*
|
||||
If we do have a root folder name, then we list its contents
|
||||
*/
|
||||
return fsAsync
|
||||
.readdir(fsPath)
|
||||
.then((fileNames) => {
|
||||
return Promise.all(
|
||||
map(fileNames, (fileName) => {
|
||||
const filePath = join(fsPath, fileName)
|
||||
return fsAsync
|
||||
.access(filePath, constants.F_OK)
|
||||
.then(() => {
|
||||
return fsAsync.stat(filePath).then((stat) => {
|
||||
const _stat = stat as unknown as FileStat
|
||||
_stat.name = fileName
|
||||
return _stat
|
||||
})
|
||||
})
|
||||
.catch(() => null)
|
||||
})
|
||||
)
|
||||
})
|
||||
.then(compact)
|
||||
}
|
||||
|
||||
async get(fileName: string): Promise<FileStat> {
|
||||
const path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName)
|
||||
const [empty, subdomain, folderName, ...fNames] = path.split('/')
|
||||
const fName = fNames.join('/')
|
||||
this.log.dbg(`get`, { _path: path, subdomain, folderName, fName, fileName })
|
||||
const { fsPath, subdomain, instance, rootFolderName, pathFromRootFolder } =
|
||||
await this._resolvePath(fileName)
|
||||
|
||||
const { dbg, error } = this.log
|
||||
.create(`get`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(fileName)
|
||||
.breadcrumb(fsPath)
|
||||
dbg(`get`)
|
||||
|
||||
/*
|
||||
If the subdomain is not specified, we are in the root
|
||||
*/
|
||||
if (!subdomain) {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
@ -118,11 +259,18 @@ export class PhFs extends FileSystem {
|
||||
name: '/',
|
||||
}
|
||||
}
|
||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
||||
|
||||
/*
|
||||
We are in a subdomain, we must have an instance
|
||||
*/
|
||||
if (!instance) {
|
||||
throw new Error(`Expected instance here`)
|
||||
}
|
||||
if (!folderName) {
|
||||
|
||||
/*
|
||||
If we don't have a root folder, we're at the instance subdomain level
|
||||
*/
|
||||
if (!rootFolderName) {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
mode: 0o755,
|
||||
@ -131,116 +279,196 @@ export class PhFs extends FileSystem {
|
||||
name: subdomain,
|
||||
}
|
||||
}
|
||||
if (!isFolder(folderName)) {
|
||||
throw new Error(`Invalid folder name`)
|
||||
}
|
||||
if (!fName) {
|
||||
|
||||
/*
|
||||
If we don't have a path beneath the root folder, we're at a root folder level
|
||||
*/
|
||||
if (!pathFromRootFolder) {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
mode: 0o755,
|
||||
size: 0,
|
||||
mtime: Date.parse(instance.updated),
|
||||
name: folderName,
|
||||
name: rootFolderName,
|
||||
}
|
||||
}
|
||||
const physicalPath = join(
|
||||
DAEMON_PB_DATA_DIR,
|
||||
instance.id,
|
||||
folderName,
|
||||
fName
|
||||
)
|
||||
this.log.dbg({ physicalPath, exists: existsSync(physicalPath) })
|
||||
|
||||
return super.get(physicalPath)
|
||||
/*
|
||||
Otherwise, this might be an actual file
|
||||
*/
|
||||
return fsAsync.stat(fsPath).then((stat) => {
|
||||
const _stat = stat as unknown as FileStat
|
||||
_stat.name = fileName
|
||||
return _stat
|
||||
})
|
||||
}
|
||||
|
||||
async write(
|
||||
fileName: string,
|
||||
options?: { append?: boolean | undefined; start?: any } | undefined
|
||||
) {
|
||||
const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName)
|
||||
const [empty, subdomain, folderName, ...fNames] = _path.split('/')
|
||||
const fName = fNames.join('/')
|
||||
this.log.dbg(`read`, { subdomain, folderName, fName })
|
||||
const { dbg, error } = this.log
|
||||
.create(`write`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(fileName)
|
||||
dbg(`write`)
|
||||
|
||||
if (!subdomain) {
|
||||
throw new Error(`Subdomain not found`)
|
||||
}
|
||||
if (!folderName) {
|
||||
throw new Error(`Folder name expected here`)
|
||||
}
|
||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
||||
if (!instance) {
|
||||
throw new Error(`Expected instance here`)
|
||||
}
|
||||
if (!isFolder(folderName)) {
|
||||
throw new Error(`Invalid folder name ${folderName}`)
|
||||
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||
fileName
|
||||
)
|
||||
const { append, start } = options || {}
|
||||
|
||||
/*
|
||||
If we are not in a root folder, disallow writing
|
||||
*/
|
||||
if (pathFromRootFolder.length === 0) {
|
||||
throw new Error(`write not allowed at this level`)
|
||||
}
|
||||
|
||||
if (fName === README_NAME) {
|
||||
throw new Error(`Can't overwrite ${fName}`)
|
||||
const stream = createWriteStream(fsPath, {
|
||||
flags: !append ? 'w+' : 'a+',
|
||||
start,
|
||||
})
|
||||
stream.once('error', () => fsAsync.unlink(fsPath))
|
||||
stream.once('close', () => stream.end())
|
||||
return {
|
||||
stream,
|
||||
clientPath,
|
||||
}
|
||||
const dir = join(DAEMON_PB_DATA_DIR, instance.id, folderName)
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
return super.write(join(dir, fName), options)
|
||||
}
|
||||
|
||||
async read(
|
||||
fileName: string,
|
||||
options: { start?: any } | undefined
|
||||
): Promise<any> {
|
||||
const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName)
|
||||
const [empty, subdomain, folderName, ...fNames] = _path.split('/')
|
||||
const fName = fNames.join('/')
|
||||
this.log.dbg(`read`, { subdomain, folderName, fName })
|
||||
const { dbg, error } = this.log
|
||||
.create(`read`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(fileName)
|
||||
dbg(`read`)
|
||||
|
||||
if (!subdomain) {
|
||||
throw new Error(`Subdomain not found`)
|
||||
}
|
||||
if (!folderName) {
|
||||
throw new Error(`Folder name expected here`)
|
||||
}
|
||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
||||
if (!instance) {
|
||||
throw new Error(`Expected instance here`)
|
||||
}
|
||||
if (!isFolder(folderName)) {
|
||||
throw new Error(`Invalid folder name ${folderName}`)
|
||||
}
|
||||
|
||||
if (fName === README_NAME) {
|
||||
return Readable.from([README_CONTENTS[folderName]])
|
||||
}
|
||||
const realPath = join(
|
||||
`/`,
|
||||
DAEMON_PB_DATA_DIR,
|
||||
instance.id,
|
||||
folderName,
|
||||
fName
|
||||
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||
fileName
|
||||
)
|
||||
this.log.dbg({ realPath })
|
||||
return super.read(realPath, options)
|
||||
|
||||
const { start } = options || {}
|
||||
|
||||
/*
|
||||
If we are not in a root folder, disallow reading
|
||||
*/
|
||||
if (pathFromRootFolder.length === 0) {
|
||||
throw new Error(`read not allowed at this level`)
|
||||
}
|
||||
|
||||
return fsAsync
|
||||
.stat(fsPath)
|
||||
.then((stat) => {
|
||||
if (stat.isDirectory()) throw new Error('Cannot read a directory')
|
||||
})
|
||||
.then(() => {
|
||||
const stream = createReadStream(fsPath, { flags: 'r', start })
|
||||
return {
|
||||
stream,
|
||||
clientPath,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<any> {
|
||||
throw new Error(`delete ${path}`)
|
||||
async delete(path: string) {
|
||||
const { dbg, error } = this.log
|
||||
.create(`delete`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(path)
|
||||
dbg(`delete`)
|
||||
|
||||
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||
path
|
||||
)
|
||||
|
||||
/*
|
||||
Disallow deleting if not inside root folder
|
||||
*/
|
||||
if (pathFromRootFolder.length === 0) {
|
||||
throw new Error(`delete not allowed at this level`)
|
||||
}
|
||||
|
||||
return fsAsync.stat(fsPath).then((stat) => {
|
||||
if (stat.isDirectory()) return fsAsync.rmdir(fsPath)
|
||||
else return fsAsync.unlink(fsPath)
|
||||
})
|
||||
}
|
||||
|
||||
async mkdir(path: string): Promise<any> {
|
||||
throw new Error(`mkdir ${path}`)
|
||||
async mkdir(path: string) {
|
||||
const { dbg, error } = this.log
|
||||
.create(`mkdir`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(path)
|
||||
dbg(`mkdir`)
|
||||
|
||||
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||
path
|
||||
)
|
||||
|
||||
/*
|
||||
Disallow making directories if not inside root folder
|
||||
*/
|
||||
if (pathFromRootFolder.length === 0) {
|
||||
throw new Error(`mkdir not allowed at this level`)
|
||||
}
|
||||
|
||||
return fsAsync.mkdir(fsPath, { recursive: true }).then(() => fsPath)
|
||||
}
|
||||
|
||||
async rename(from: string, to: string): Promise<any> {
|
||||
throw new Error(`rename ${from} ${to}`)
|
||||
async rename(from: string, to: string) {
|
||||
const { dbg, error } = this.log
|
||||
.create(`rename`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(from)
|
||||
.breadcrumb(to)
|
||||
dbg(`rename`)
|
||||
|
||||
const { fsPath: fromPath, pathFromRootFolder: fromPathFromRootFolder } =
|
||||
await this._resolvePath(from)
|
||||
|
||||
const { fsPath: toPath, pathFromRootFolder: toPathFromRootFolder } =
|
||||
await this._resolvePath(from)
|
||||
|
||||
/*
|
||||
Disallow making directories if not inside root folder
|
||||
*/
|
||||
if (fromPathFromRootFolder.length === 0) {
|
||||
throw new Error(`rename not allowed at this level`)
|
||||
}
|
||||
if (toPathFromRootFolder.length === 0) {
|
||||
throw new Error(`rename not allowed at this level`)
|
||||
}
|
||||
|
||||
return fsAsync.rename(fromPath, toPath)
|
||||
}
|
||||
|
||||
async chmod(path: string, mode: string): Promise<any> {
|
||||
throw new Error(`chmod ${path} ${mode}`)
|
||||
async chmod(path: string, mode: Mode) {
|
||||
const { dbg, error } = this.log
|
||||
.create(`chmod`)
|
||||
.breadcrumb(`cwd:${this.cwd}`)
|
||||
.breadcrumb(path)
|
||||
.breadcrumb(mode.toString())
|
||||
dbg(`chmod`)
|
||||
|
||||
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||
path
|
||||
)
|
||||
|
||||
/*
|
||||
Disallow making directories if not inside root folder
|
||||
*/
|
||||
if (pathFromRootFolder.length === 0) {
|
||||
throw new Error(`chmod not allowed at this level`)
|
||||
}
|
||||
return // noop
|
||||
return fsAsync.chmod(fsPath, mode)
|
||||
}
|
||||
|
||||
getUniqueName(fileName: string): string {
|
||||
throw new Error(`getUniqueName ${fileName}`)
|
||||
getUniqueName() {
|
||||
return nanoid(30)
|
||||
}
|
||||
}
|
||||
|
13
packages/daemon/src/services/FtpService/fs-async.ts
Normal file
13
packages/daemon/src/services/FtpService/fs-async.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const stat = promisify(fs.stat)
|
||||
const readdir = promisify(fs.readdir)
|
||||
const access = promisify(fs.access)
|
||||
const unlink = promisify(fs.unlink)
|
||||
const rmdir = promisify(fs.rmdir)
|
||||
const mkdir = promisify(fs.mkdir)
|
||||
const rename = promisify(fs.rename)
|
||||
const chmod = promisify(fs.chmod)
|
||||
|
||||
export { stat, readdir, access, unlink, rmdir, mkdir, rename, chmod }
|
@ -9,6 +9,7 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user