From 7268d7ab72810ca12c9586364b536beec92cc56b Mon Sep 17 00:00:00 2001 From: Sean Zellmer Date: Thu, 26 Sep 2024 21:16:46 -0500 Subject: [PATCH 1/5] Add `drive.rename(oldPath, newPath)` Only updates the file entry in the hyperbee. --- README.md | 10 ++++++++-- index.js | 6 ++++++ test.js | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5c91c5..1eb6c0e 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,20 @@ console.log(entry) // => { seq, key, value: { executable, linkname, blob, metada await drive.del('/images/old-logo.png') +await drive.rename('/images/blob.txt', '/images/example.txt') + await drive.symlink('/images/logo.shortcut', '/images/logo.png') for await (const file of drive.list('/images')) { console.log('list', file) // => { key, value } } -const rs = drive.createReadStream('/blob.txt') +const rs = drive.createReadStream('/images/example.txt') for await (const chunk of rs) { console.log('rs', chunk) // => } -const ws = drive.createWriteStream('/blob.txt') +const ws = drive.createWriteStream('/images/example.txt') ws.write('new example') ws.end() ws.once('close', () => console.log('file saved')) @@ -204,6 +206,10 @@ A `blobs: ` option can be passed in if you know the corresponding blobs Purge both cores (db and blobs) from your storage, completely removing all the drive's data. +#### `await drive.rename(oldPath, newPath, [options])` + +Renames the `oldPath` entry in the drive to be `newPath`. `options` are the same as in `get`. + #### `await drive.symlink(path, linkname)` Creates an entry in drive at `path` that points to the entry at `linkname`. diff --git a/index.js b/index.js index 99b4d64..6e04966 100644 --- a/index.js +++ b/index.js @@ -358,6 +358,12 @@ module.exports = class Hyperdrive extends ReadyResource { await Promise.all(proms) } + async rename (oldPath, newPath, opts) { + const existingEntry = await this.entry(oldPath, opts) + await this.db.del(std(oldPath, false), { keyEncoding }) + return this.db.put(std(newPath, false), existingEntry.value, { keyEncoding }) + } + async symlink (name, dst, { metadata = null } = {}) { return this.db.put(std(name, false), { executable: false, linkname: dst, blob: null, metadata }, { keyEncoding }) } diff --git a/test.js b/test.js index 38bcfc0..b54a086 100644 --- a/test.js +++ b/test.js @@ -185,6 +185,17 @@ test('drive.del() deletes entry at path', async (t) => { t.is(entry, null) }) +test('drive.rename(oldPath, newPath) updates the entry at to use as a key', async (t) => { + const { drive } = await testenv(t.teardown) + const buf = fs.readFileSync(__filename) + await drive.put(__filename, buf) + await drive.rename(__filename, 'new-file') + const resultOld = await drive.get(__filename) + t.is(resultOld, null) + const resultNew = await drive.get('new-file') + t.is(b4a.compare(buf, resultNew), 0) +}) + test('drive.symlink(from, to) updates the entry at to include a reference for ', async (t) => { const { drive } = await testenv(t.teardown) const buf = fs.readFileSync(__filename) From 0d7ca730538f17d9efdf9f02c048dec535c6d425 Mon Sep 17 00:00:00 2001 From: Sean Zellmer Date: Wed, 16 Oct 2024 14:40:58 -0500 Subject: [PATCH 2/5] Batch rename read, del & put substeps fixing race condition Previously renaming an entry twice at the same time could result in a race condition where the two entries were created for the same `oldPath` etc. --- index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 6e04966..c35b9f3 100644 --- a/index.js +++ b/index.js @@ -359,9 +359,11 @@ module.exports = class Hyperdrive extends ReadyResource { } async rename (oldPath, newPath, opts) { - const existingEntry = await this.entry(oldPath, opts) - await this.db.del(std(oldPath, false), { keyEncoding }) - return this.db.put(std(newPath, false), existingEntry.value, { keyEncoding }) + const b = this.batch() + const existingEntry = await b.entry(oldPath, opts) + await b.db.del(std(oldPath, false), { keyEncoding }) + await b.db.put(std(newPath, false), existingEntry.value, { keyEncoding }) + return b.flush() } async symlink (name, dst, { metadata = null } = {}) { From 1ec8474e6d42ae612a37624dad07478ec8d6f200 Mon Sep 17 00:00:00 2001 From: Sean Zellmer Date: Wed, 16 Oct 2024 14:59:05 -0500 Subject: [PATCH 3/5] Return early when renaming non-existing entry --- index.js | 2 ++ test.js | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/index.js b/index.js index c35b9f3..300a568 100644 --- a/index.js +++ b/index.js @@ -361,6 +361,8 @@ module.exports = class Hyperdrive extends ReadyResource { async rename (oldPath, newPath, opts) { const b = this.batch() const existingEntry = await b.entry(oldPath, opts) + if (existingEntry === null) return b.flush() + await b.db.del(std(oldPath, false), { keyEncoding }) await b.db.put(std(newPath, false), existingEntry.value, { keyEncoding }) return b.flush() diff --git a/test.js b/test.js index b54a086..784d600 100644 --- a/test.js +++ b/test.js @@ -196,6 +196,17 @@ test('drive.rename(oldPath, newPath) updates the entry at to use ', async (t) => { + const { drive } = await testenv(t.teardown) + const buf = fs.readFileSync(__filename) + await drive.put(__filename, buf) + await drive.rename('non-existing', 'new-file') + const resultOld = await drive.get(__filename) + t.is(b4a.compare(buf, resultOld), 0) + const resultNew = await drive.get('new-file') + t.is(resultNew, null) +}) + test('drive.symlink(from, to) updates the entry at to include a reference for ', async (t) => { const { drive } = await testenv(t.teardown) const buf = fs.readFileSync(__filename) From bcae1588a1cbadb89905923faf4a3716ed3d5b93 Mon Sep 17 00:00:00 2001 From: Sean Zellmer Date: Wed, 16 Oct 2024 15:00:16 -0500 Subject: [PATCH 4/5] Add test for renaming a symlink --- test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test.js b/test.js index 784d600..1d8f373 100644 --- a/test.js +++ b/test.js @@ -207,6 +207,17 @@ test('drive.rename(oldPath, newPath) does nothing if renaming a non-existing
    { + const { drive } = await testenv(t.teardown) + const buf = fs.readFileSync(__filename) + await drive.put(__filename, buf) + await drive.symlink('pointer', __filename) + await drive.rename('pointer', 'new-pointer') + const entry = await drive.entry('new-pointer') + t.is(entry.value.linkname, __filename) + t.is(b4a.compare(buf, await drive.get(entry.value.linkname)), 0) +}) + test('drive.symlink(from, to) updates the entry at to include a reference for ', async (t) => { const { drive } = await testenv(t.teardown) const buf = fs.readFileSync(__filename) From 51582cbf5a6948d251335bc2ac969dee12fdc827 Mon Sep 17 00:00:00 2001 From: Sean Zellmer Date: Wed, 16 Oct 2024 15:00:43 -0500 Subject: [PATCH 5/5] Clarify that `rename()` only applies to a single entry --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1eb6c0e..50604b7 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Purge both cores (db and blobs) from your storage, completely removing all the d #### `await drive.rename(oldPath, newPath, [options])` -Renames the `oldPath` entry in the drive to be `newPath`. `options` are the same as in `get`. +Renames a single `oldPath` entry in the drive to be `newPath`. `options` are the same as in `get`. #### `await drive.symlink(path, linkname)`