feat(fs): prefix get/set → addPrefix method (#3766)

This method returns a new object which will transparently prefix all paths.

It is better than previous API because:

- the same handler can be reused and will not be invalidated when changing the prefix
- Abstract and implementations can call high-level methods without worrying about doubling the prefix
This commit is contained in:
Julien Fontanet 2018-12-06 17:22:37 +01:00 committed by Pierre Donias
parent 11f742b020
commit 11cff2c065
2 changed files with 100 additions and 49 deletions

View File

@ -28,11 +28,6 @@ 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
const ignoreEnoent = error => {
@ -41,6 +36,37 @@ const ignoreEnoent = error => {
}
}
class PrefixWrapper {
constructor(remote, prefix) {
this._prefix = prefix
this._remote = remote
}
get type() {
return this._remote.type
}
// necessary to remove the prefix from the path with `prependDir` option
async list(dir, opts) {
const entries = await this._remote.list(this._resolve(dir), opts)
if (opts != null && opts.prependDir) {
const n = this._prefix.length
entries.forEach((entry, i, entries) => {
entries[i] = entry.slice(n)
})
}
return entries
}
rename(oldPath, newPath) {
return this._remote.rename(this._resolve(oldPath), this._resolve(newPath))
}
_resolve(path) {
return this._prefix + normalizePath(path)
}
}
export default class RemoteHandlerAbstract {
_remote: Object
_timeout: number
@ -55,25 +81,19 @@ export default class RemoteHandlerAbstract {
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
this[kPrefix] = ''
}
// Public members
get prefix(): string {
return this[kPrefix]
}
set prefix(prefix: string) {
prefix = normalizePath(prefix)
this[kPrefix] = prefix === '/' ? '' : prefix
}
get type(): string {
throw new Error('Not implemented')
}
addPrefix(prefix: string) {
prefix = normalizePath(prefix)
return prefix === '/' ? this : new PrefixWrapper(this, prefix)
}
async closeFile(fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), this._timeout)
}
@ -83,7 +103,7 @@ export default class RemoteHandlerAbstract {
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
if (typeof file === 'string') {
file = this[kResolve](file)
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(
@ -122,7 +142,7 @@ export default class RemoteHandlerAbstract {
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
): Promise<LaxReadable> {
if (typeof file === 'string') {
file = this[kResolve](file)
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout
@ -186,7 +206,7 @@ export default class RemoteHandlerAbstract {
async getSize(file: File): Promise<number> {
return timeout.call(
this._getSize(typeof file === 'string' ? this[kResolve](file) : file),
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
this._timeout
)
}
@ -199,7 +219,7 @@ export default class RemoteHandlerAbstract {
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
const virtualDir = normalizePath(dir)
dir = this[kResolve](dir)
dir = normalizePath(dir)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
@ -216,7 +236,7 @@ export default class RemoteHandlerAbstract {
}
async openFile(path: string, flags?: string): Promise<FileDescriptor> {
path = this[kResolve](path)
path = normalizePath(path)
return {
fd: await timeout.call(this._openFile(path, flags), this._timeout),
@ -229,7 +249,7 @@ export default class RemoteHandlerAbstract {
data: Data,
{ flags = 'wx' }: { flags?: string } = {}
): Promise<void> {
return this._outputFile(this[kResolve](file), data, { flags })
return this._outputFile(normalizePath(file), data, { flags })
}
async read(
@ -238,7 +258,7 @@ export default class RemoteHandlerAbstract {
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
return this._read(
typeof file === 'string' ? this[kResolve](file) : file,
typeof file === 'string' ? normalizePath(file) : file,
buffer,
position
)
@ -248,11 +268,11 @@ export default class RemoteHandlerAbstract {
file: string,
{ flags = 'r' }: { flags?: string } = {}
): Promise<Buffer> {
return this._readFile(this[kResolve](file), { flags })
return this._readFile(normalizePath(file), { flags })
}
async refreshChecksum(path: string): Promise<void> {
path = this[kResolve](path)
path = normalizePath(path)
const stream = (await this._createReadStream(path, { flags: 'r' })).pipe(
createChecksumStream()
@ -268,8 +288,8 @@ export default class RemoteHandlerAbstract {
newPath: string,
{ checksum = false }: Object = {}
) {
oldPath = this[kResolve](oldPath)
newPath = this[kResolve](newPath)
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
@ -283,13 +303,13 @@ export default class RemoteHandlerAbstract {
async rmdir(dir: string): Promise<void> {
await timeout.call(
this._rmdir(this[kResolve](dir)).catch(ignoreEnoent),
this._rmdir(normalizePath(dir)).catch(ignoreEnoent),
this._timeout
)
}
async rmtree(dir: string): Promise<void> {
await this._rmtree(this[kResolve](dir))
await this._rmtree(normalizePath(dir))
}
// Asks the handler to sync the state of the effective remote with its'
@ -299,7 +319,7 @@ export default class RemoteHandlerAbstract {
}
async test(): Promise<Object> {
const testFileName = this[kResolve](`${Date.now()}.test`)
const testFileName = normalizePath(`${Date.now()}.test`)
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
let step = 'write'
try {
@ -325,7 +345,7 @@ export default class RemoteHandlerAbstract {
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = this[kResolve](file)
file = normalizePath(file)
if (checksum) {
ignoreErrors.call(this._unlink(checksumFile(file)))
@ -420,12 +440,38 @@ export default class RemoteHandlerAbstract {
async _unlink(file: string): Promise<void> {
throw new Error('Not implemented')
}
// Private members
[kResolve](path: string): string {
path = normalizePath(path)
const prefix = this[kPrefix]
return path === '/' ? prefix : prefix + path
}
}
function createPrefixWrapperMethods() {
const pPw = PrefixWrapper.prototype
const pRha = RemoteHandlerAbstract.prototype
const {
defineProperty,
getOwnPropertyDescriptor,
prototype: { hasOwnProperty },
} = Object
Object.getOwnPropertyNames(pRha).forEach(name => {
let descriptor, value
if (
hasOwnProperty.call(pPw, name) ||
name[0] === '_' ||
typeof (value = (descriptor = getOwnPropertyDescriptor(pRha, name))
.value) !== 'function'
) {
return
}
descriptor.value = function() {
let path
if (arguments.length !== 0 && typeof (path = arguments[0]) === 'string') {
arguments[0] = this._resolve(path)
}
return value.apply(this._remote, arguments)
}
defineProperty(pPw, name, descriptor)
})
}
createPrefixWrapperMethods()

View File

@ -47,10 +47,8 @@ handlers.forEach(url => {
let handler
beforeAll(async () => {
handler = getHandler({ url })
handler = getHandler({ url }).addPrefix(`xo-fs-tests-${Date.now()}`)
await handler.sync()
handler.prefix = `xo-fs-tests-${Date.now()}`
})
afterAll(async () => {
await handler.forget()
@ -64,13 +62,13 @@ handlers.forEach(url => {
await handler.unlink('file')
})
afterEach(async () => {
// 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.rmtree(prefix)
handler.prefix = prefix
await handler.rmtree('.')
})
describe('#type', () => {
it('returns the type of the remote', () => {
expect(typeof handler.type).toBe('string')
})
})
describe('#createOutputStream()', () => {
@ -139,6 +137,13 @@ handlers.forEach(url => {
await handler.outputFile('file', TEST_DATA)
await expect(await handler.list('.')).toEqual(['file'])
})
it('can prepend the directory to entries', async () => {
await handler.outputFile('dir/file', '')
expect(await handler.list('dir', { prependDir: true })).toEqual([
'/dir/file',
])
})
})
describe('#outputFile()', () => {