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 {
|
export enum FolderNames {
|
||||||
PbData = 'pb_data',
|
PbData = 'pb_data',
|
||||||
PbStatic = 'pb_static',
|
// PbStatic = 'pb_static',
|
||||||
PbMigrations = 'pb_migrations',
|
PbMigrations = 'pb_migrations',
|
||||||
PbWorker = 'worker',
|
PbWorker = 'worker',
|
||||||
PbBackup = 'backup',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const README_CONTENTS: { [_ in FolderNames]: string } = {
|
export const INSTANCE_ROOT_FOLDER_NAMES: FolderNames[] = [
|
||||||
[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,
|
|
||||||
FolderNames.PbData,
|
FolderNames.PbData,
|
||||||
FolderNames.PbStatic,
|
// FolderNames.PbStatic,
|
||||||
FolderNames.PbWorker,
|
FolderNames.PbWorker,
|
||||||
FolderNames.PbMigrations,
|
FolderNames.PbMigrations,
|
||||||
]
|
]
|
||||||
|
|
||||||
export function isFolder(name: string): name is FolderNames {
|
export function isInstanceRootFolder(name: string): name is FolderNames {
|
||||||
return FOLDER_NAMES.includes(name as FolderNames)
|
return INSTANCE_ROOT_FOLDER_NAMES.includes(name as FolderNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tls = {
|
const tls = {
|
||||||
|
@ -1,59 +1,180 @@
|
|||||||
import { DAEMON_PB_DATA_DIR } from '$constants'
|
import { DAEMON_PB_DATA_DIR } from '$constants'
|
||||||
import { Logger } from '@pockethost/common'
|
import { Logger } from '@pockethost/common'
|
||||||
import { existsSync, mkdirSync } from 'fs'
|
import { compact, map } from '@s-libs/micro-dash'
|
||||||
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
|
|
||||||
import { join } from 'path'
|
|
||||||
import { Readable } from 'stream'
|
|
||||||
import { PocketbaseClientApi } from '../clientService/PbClient'
|
|
||||||
import {
|
import {
|
||||||
FOLDER_NAMES,
|
constants,
|
||||||
isFolder,
|
createReadStream,
|
||||||
README_CONTENTS,
|
createWriteStream,
|
||||||
README_NAME,
|
existsSync,
|
||||||
} from './FtpService'
|
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 log: Logger
|
||||||
private client: PocketbaseClientApi
|
private client: PocketbaseClientApi
|
||||||
|
connection: FtpConnection
|
||||||
|
cwd: string
|
||||||
|
private _root: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
connection: FtpConnection,
|
connection: FtpConnection,
|
||||||
client: PocketbaseClientApi,
|
client: PocketbaseClientApi,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
) {
|
) {
|
||||||
super(connection, { root: '/', cwd: '/' })
|
const cwd = `/`
|
||||||
|
const root = DAEMON_PB_DATA_DIR
|
||||||
|
this.connection = connection
|
||||||
this.client = client
|
this.client = client
|
||||||
this.log = logger.create(`PhFs`)
|
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> {
|
get root() {
|
||||||
this.log.dbg(`chdir`, path)
|
return this._root
|
||||||
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 })
|
|
||||||
|
|
||||||
if (subdomain) {
|
async _resolvePath(path = '.') {
|
||||||
const instance = await this.client.getInstanceBySubdomain(subdomain)
|
const { dbg } = this.log.create(`_resolvePath`)
|
||||||
if (!instance) {
|
|
||||||
throw new Error(`Subdomain not found`)
|
// 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[]> {
|
currentDirectory() {
|
||||||
this.log.dbg(`list ${path}`, this.cwd)
|
return 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 })
|
|
||||||
|
|
||||||
|
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 === '') {
|
if (subdomain === '') {
|
||||||
const instances = await this.client.getInstances()
|
const instances = await this.client.getInstances()
|
||||||
return instances.map((i) => {
|
return instances.map((i) => {
|
||||||
@ -66,15 +187,22 @@ export class PhFs extends FileSystem {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!subdomain) {
|
|
||||||
throw new Error(`Subdomain expected in ${_path}`)
|
/*
|
||||||
}
|
If we have a subdomain, then it should have resolved to an instance owned by that user
|
||||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
*/
|
||||||
if (!instance) {
|
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,
|
isDirectory: () => true,
|
||||||
mode: 0o755,
|
mode: 0o755,
|
||||||
size: 0,
|
size: 0,
|
||||||
@ -82,33 +210,46 @@ export class PhFs extends FileSystem {
|
|||||||
name: name,
|
name: name,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if (!isFolder(folderName)) {
|
|
||||||
throw new Error(`Top level folder name ${folderName} not allowed.`)
|
/*
|
||||||
}
|
If we do have a root folder name, then we list its contents
|
||||||
const dir = join(DAEMON_PB_DATA_DIR, instance.id, folderName, ...restOfPath)
|
*/
|
||||||
this.log.dbg({ dir, exists: existsSync(dir) })
|
return fsAsync
|
||||||
return [
|
.readdir(fsPath)
|
||||||
...(restOfPath.length === 0
|
.then((fileNames) => {
|
||||||
? [
|
return Promise.all(
|
||||||
{
|
map(fileNames, (fileName) => {
|
||||||
isDirectory: () => false,
|
const filePath = join(fsPath, fileName)
|
||||||
mode: 0o444,
|
return fsAsync
|
||||||
size: README_CONTENTS[folderName].length,
|
.access(filePath, constants.F_OK)
|
||||||
mtime: Date.parse(instance.updated),
|
.then(() => {
|
||||||
name: README_NAME,
|
return fsAsync.stat(filePath).then((stat) => {
|
||||||
},
|
const _stat = stat as unknown as FileStat
|
||||||
]
|
_stat.name = fileName
|
||||||
: []),
|
return _stat
|
||||||
...(existsSync(dir) ? await super.list(dir) : []),
|
})
|
||||||
]
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then(compact)
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(fileName: string): Promise<FileStat> {
|
async get(fileName: string): Promise<FileStat> {
|
||||||
const path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName)
|
const { fsPath, subdomain, instance, rootFolderName, pathFromRootFolder } =
|
||||||
const [empty, subdomain, folderName, ...fNames] = path.split('/')
|
await this._resolvePath(fileName)
|
||||||
const fName = fNames.join('/')
|
|
||||||
this.log.dbg(`get`, { _path: path, subdomain, folderName, fName, 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) {
|
if (!subdomain) {
|
||||||
return {
|
return {
|
||||||
isDirectory: () => true,
|
isDirectory: () => true,
|
||||||
@ -118,11 +259,18 @@ export class PhFs extends FileSystem {
|
|||||||
name: '/',
|
name: '/',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
|
||||||
|
/*
|
||||||
|
We are in a subdomain, we must have an instance
|
||||||
|
*/
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error(`Expected instance here`)
|
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 {
|
return {
|
||||||
isDirectory: () => true,
|
isDirectory: () => true,
|
||||||
mode: 0o755,
|
mode: 0o755,
|
||||||
@ -131,116 +279,196 @@ export class PhFs extends FileSystem {
|
|||||||
name: subdomain,
|
name: subdomain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isFolder(folderName)) {
|
|
||||||
throw new Error(`Invalid folder name`)
|
/*
|
||||||
}
|
If we don't have a path beneath the root folder, we're at a root folder level
|
||||||
if (!fName) {
|
*/
|
||||||
|
if (!pathFromRootFolder) {
|
||||||
return {
|
return {
|
||||||
isDirectory: () => true,
|
isDirectory: () => true,
|
||||||
mode: 0o755,
|
mode: 0o755,
|
||||||
size: 0,
|
size: 0,
|
||||||
mtime: Date.parse(instance.updated),
|
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(
|
async write(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
options?: { append?: boolean | undefined; start?: any } | undefined
|
options?: { append?: boolean | undefined; start?: any } | undefined
|
||||||
) {
|
) {
|
||||||
const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName)
|
const { dbg, error } = this.log
|
||||||
const [empty, subdomain, folderName, ...fNames] = _path.split('/')
|
.create(`write`)
|
||||||
const fName = fNames.join('/')
|
.breadcrumb(`cwd:${this.cwd}`)
|
||||||
this.log.dbg(`read`, { subdomain, folderName, fName })
|
.breadcrumb(fileName)
|
||||||
|
dbg(`write`)
|
||||||
|
|
||||||
if (!subdomain) {
|
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||||
throw new Error(`Subdomain not found`)
|
fileName
|
||||||
}
|
)
|
||||||
if (!folderName) {
|
const { append, start } = options || {}
|
||||||
throw new Error(`Folder name expected here`)
|
|
||||||
}
|
/*
|
||||||
const [instance, user] = await this.client.getInstanceBySubdomain(subdomain)
|
If we are not in a root folder, disallow writing
|
||||||
if (!instance) {
|
*/
|
||||||
throw new Error(`Expected instance here`)
|
if (pathFromRootFolder.length === 0) {
|
||||||
}
|
throw new Error(`write not allowed at this level`)
|
||||||
if (!isFolder(folderName)) {
|
|
||||||
throw new Error(`Invalid folder name ${folderName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fName === README_NAME) {
|
const stream = createWriteStream(fsPath, {
|
||||||
throw new Error(`Can't overwrite ${fName}`)
|
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(
|
async read(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
options: { start?: any } | undefined
|
options: { start?: any } | undefined
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName)
|
const { dbg, error } = this.log
|
||||||
const [empty, subdomain, folderName, ...fNames] = _path.split('/')
|
.create(`read`)
|
||||||
const fName = fNames.join('/')
|
.breadcrumb(`cwd:${this.cwd}`)
|
||||||
this.log.dbg(`read`, { subdomain, folderName, fName })
|
.breadcrumb(fileName)
|
||||||
|
dbg(`read`)
|
||||||
|
|
||||||
if (!subdomain) {
|
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(
|
||||||
throw new Error(`Subdomain not found`)
|
fileName
|
||||||
}
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
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> {
|
async delete(path: string) {
|
||||||
throw new Error(`delete ${path}`)
|
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> {
|
async mkdir(path: string) {
|
||||||
throw new Error(`mkdir ${path}`)
|
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> {
|
async rename(from: string, to: string) {
|
||||||
throw new Error(`rename ${from} ${to}`)
|
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> {
|
async chmod(path: string, mode: Mode) {
|
||||||
throw new Error(`chmod ${path} ${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 {
|
getUniqueName() {
|
||||||
throw new Error(`getUniqueName ${fileName}`)
|
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,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user