feat(fs): prefix support (#3759)

It is a prefix that will be used as root for all other paths.
This commit is contained in:
Julien Fontanet 2018-12-05 13:51:38 +01:00 committed by GitHub
parent d74a5d73f0
commit fd93dfbc18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 70 additions and 43 deletions

View File

@ -26,6 +26,11 @@ const checksumFile = file => file + '.checksum'
// - always starts with `/`
const normalizePath = path => resolve('/', path)
// use symbols for private members to avoid any conflicts with inheriting
// classes
const kPrefix = Symbol('prefix')
const kResolve = Symbol('resolve')
const DEFAULT_TIMEOUT = 6e5 // 10 min
export default class RemoteHandlerAbstract {
@ -42,6 +47,21 @@ export default class RemoteHandlerAbstract {
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
this[kPrefix] = ''
}
get prefix() {
return this[kPrefix]
}
set prefix(prefix) {
prefix = normalizePath(prefix)
this[kPrefix] = prefix === '/' ? '' : prefix
}
[kResolve](path) {
return this[kPrefix] + normalizePath(path)
}
get type(): string {
@ -71,7 +91,7 @@ export default class RemoteHandlerAbstract {
}
async test(): Promise<Object> {
const testFileName = `/${Date.now()}.test`
const testFileName = `${Date.now()}.test`
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
let step = 'write'
try {
@ -101,7 +121,7 @@ export default class RemoteHandlerAbstract {
data: Data,
{ flags = 'wx' }: { flags?: string } = {}
): Promise<void> {
return this._outputFile(normalizePath(file), data, { flags })
return this._outputFile(this[kResolve](file), data, { flags })
}
async _outputFile(file: string, data: Data, options?: Object): Promise<void> {
@ -117,7 +137,7 @@ export default class RemoteHandlerAbstract {
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
return this._read(
typeof file === 'string' ? normalizePath(file) : file,
typeof file === 'string' ? this[kResolve](file) : file,
buffer,
position
)
@ -135,7 +155,7 @@ export default class RemoteHandlerAbstract {
file: string,
{ flags = 'r' }: { flags?: string } = {}
): Promise<Buffer> {
return this._readFile(normalizePath(file), { flags })
return this._readFile(this[kResolve](file), { flags })
}
_readFile(file: string, options?: Object): Promise<Buffer> {
@ -147,8 +167,8 @@ export default class RemoteHandlerAbstract {
newPath: string,
{ checksum = false }: Object = {}
) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
oldPath = this[kResolve](oldPath)
newPath = this[kResolve](newPath)
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
@ -168,7 +188,7 @@ export default class RemoteHandlerAbstract {
dir: string,
{ recursive = false }: { recursive?: boolean } = {}
) {
dir = normalizePath(dir)
dir = this[kResolve](dir)
await (recursive ? this._rmtree(dir) : this._rmdir(dir))
}
@ -204,7 +224,8 @@ export default class RemoteHandlerAbstract {
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
dir = normalizePath(dir)
const virtualDir = normalizePath(dir)
dir = this[kResolve](dir)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
@ -213,7 +234,7 @@ export default class RemoteHandlerAbstract {
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = dir + '/' + entry
entries[i] = virtualDir + '/' + entry
})
}
@ -229,7 +250,7 @@ export default class RemoteHandlerAbstract {
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
): Promise<LaxReadable> {
if (typeof file === 'string') {
file = normalizePath(file)
file = this[kResolve](file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout
@ -290,7 +311,7 @@ export default class RemoteHandlerAbstract {
}
async openFile(path: string, flags?: string): Promise<FileDescriptor> {
path = normalizePath(path)
path = this[kResolve](path)
return {
fd: await timeout.call(this._openFile(path, flags), this._timeout),
@ -311,7 +332,7 @@ export default class RemoteHandlerAbstract {
}
async refreshChecksum(path: string): Promise<void> {
path = normalizePath(path)
path = this[kResolve](path)
const stream = (await this._createReadStream(path, { flags: 'r' })).pipe(
createChecksumStream()
@ -327,7 +348,7 @@ export default class RemoteHandlerAbstract {
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
if (typeof file === 'string') {
file = normalizePath(file)
file = this[kResolve](file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(
@ -369,7 +390,7 @@ export default class RemoteHandlerAbstract {
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = normalizePath(file)
file = this[kResolve](file)
if (checksum) {
ignoreErrors.call(this._unlink(checksumFile(file)))
@ -384,7 +405,7 @@ export default class RemoteHandlerAbstract {
async getSize(file: File): Promise<number> {
return timeout.call(
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
this._getSize(typeof file === 'string' ? this[kResolve](file) : file),
this._timeout
)
}

View File

@ -39,12 +39,12 @@ if (process.env.xo_fs_smb) handlers.push(process.env.xo_fs_smb)
handlers.forEach(url => {
describe(url, () => {
let handler
const testDir = `xo-fs-tests-${Date.now()}`
const testFile = `${testDir}/file`
beforeAll(async () => {
handler = getHandler({ url })
await handler.sync()
handler.prefix = `xo-fs-tests-${Date.now()}`
})
afterAll(async () => {
await handler.forget()
@ -52,11 +52,17 @@ handlers.forEach(url => {
})
afterEach(async () => {
await handler.rmdir(testDir, { recursive: true }).catch(error => {
// don't use the prefix feature for the final clean to avoid deleting
// everything on the remote if it's broken
const { prefix } = handler
expect(prefix).not.toBe('/')
handler.prefix = '/'
await handler.rmdir(prefix, { recursive: true }).catch(error => {
if (error.code !== 'ENOENT') {
throw error
}
})
handler.prefix = prefix
})
describe('#test()', () => {
@ -69,41 +75,41 @@ handlers.forEach(url => {
describe('#outputFile()', () => {
it('writes data to a file', async () => {
await handler.outputFile(testFile, TEST_DATA)
expect(await handler.readFile(testFile)).toEqual(TEST_DATA)
await handler.outputFile('file', TEST_DATA)
expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('throws on existing files', async () => {
await handler.outputFile(testFile, '')
const error = await rejectionOf(handler.outputFile(testFile, ''))
await handler.outputFile('file', '')
const error = await rejectionOf(handler.outputFile('file', ''))
expect(error.code).toBe('EEXIST')
})
})
describe('#readFile', () => {
it('returns a buffer containing the contents of the file', async () => {
await handler.outputFile(testFile, TEST_DATA)
expect(await handler.readFile(testFile)).toEqual(TEST_DATA)
await handler.outputFile('file', TEST_DATA)
expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('throws on missing file', async () => {
const error = await rejectionOf(handler.readFile(testFile))
const error = await rejectionOf(handler.readFile('file'))
expect(error.code).toBe('ENOENT')
})
})
describe('#list()', () => {
it(`should list the content of folder`, async () => {
await handler.outputFile(testFile, TEST_DATA)
await expect(await handler.list(testDir)).toEqual(['file'])
await handler.outputFile('file', TEST_DATA)
await expect(await handler.list('.')).toEqual(['file'])
})
})
describe('#createReadStream()', () => {
it(`should return a stream`, async () => {
await handler.outputFile(testFile, TEST_DATA)
await handler.outputFile('file', TEST_DATA)
const buffer = await getStream.buffer(
await handler.createReadStream(testFile)
await handler.createReadStream('file')
)
await expect(buffer).toEqual(TEST_DATA)
@ -111,43 +117,43 @@ handlers.forEach(url => {
})
describe('#getSize()', () => {
it(`should return the correct size`, async () => {
await handler.outputFile(testFile, TEST_DATA)
expect(await handler.getSize(testFile)).toEqual(TEST_DATA.length)
await handler.outputFile('file', TEST_DATA)
expect(await handler.getSize('file')).toEqual(TEST_DATA.length)
})
})
describe('#rename()', () => {
it(`should rename the file`, async () => {
await handler.outputFile(testFile, TEST_DATA)
await handler.rename(testFile, `${testDir}/file2`)
await handler.outputFile('file', TEST_DATA)
await handler.rename('file', `file2`)
expect(await handler.list(testDir)).toEqual(['file2'])
expect(await handler.readFile(`${testDir}/file2`)).toEqual(TEST_DATA)
expect(await handler.list('.')).toEqual(['file2'])
expect(await handler.readFile(`file2`)).toEqual(TEST_DATA)
})
})
describe('#unlink()', () => {
it(`should remove the file`, async () => {
await handler.outputFile(testFile, TEST_DATA)
await handler.unlink(testFile)
await handler.outputFile('file', TEST_DATA)
await handler.unlink('file')
await expect(await handler.list(testDir)).toEqual([])
await expect(await handler.list('.')).toEqual([])
})
})
describe('#rmdir()', () => {
it(`should remove folder resursively`, async () => {
await handler.outputFile(testFile, TEST_DATA)
await handler.rmdir(testDir, { recursive: true })
await handler.outputFile('file', TEST_DATA)
await handler.rmdir('.', { recursive: true })
const error = await rejectionOf(handler.list(testDir))
const error = await rejectionOf(handler.list('.'))
expect(error.code).toBe('ENOENT')
})
it(`should throw an error when recursive is false`, async () => {
await handler.outputFile(testFile, TEST_DATA)
await handler.outputFile('file', TEST_DATA)
const error = await rejectionOf(handler.rmdir(testDir))
const error = await rejectionOf(handler.rmdir('.'))
await expect(error.code).toEqual('ENOTEMPTY')
})
})