Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose hyperdrive drive.diff() API #90

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ From there, you can use `GET` and `HEAD` requests with allt he same headers and

Note that you cannot `PUT` or `DELETE` data in a versioned folder.

### `fetch('hyper://NAME/$/diff/OLD_VERSION..NEW_VERSION/folder/')`
josephmturner marked this conversation as resolved.
Show resolved Hide resolved

You can get a recursive diff of the changes inside a folder between
two version of a hyperdrive by sending a `GET` request to paths inside
the special `/$/diff` folder. `OLD_VERSION` and `NEW_VERSION` should
be numbers representing the versions to compare.

Response body is the JSON object returned by
[`drive.diff`](https://docs.holepunch.to/building-blocks/hyperdrive#const-stream-drive.diff-version-folder-options).

## Limitations:

Expand Down
50 changes: 50 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const EXTENSION_EVENT = 'extension-message'
const VERSION_FOLDER_NAME = 'version'
const PEER_OPEN = 'peer-open'
const PEER_REMOVE = 'peer-remove'
const DIFF_FOLDER_NAME = 'diff'

const MIME_TEXT_PLAIN = 'text/plain; charset=utf-8'
const MIME_APPLICATION_JSON = 'application/json'
Expand Down Expand Up @@ -103,6 +104,7 @@ export default async function makeHyperFetch ({

router.head(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, headFilesVersioned)
router.get(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, getFilesVersioned)
router.get(`hyper://*/${SPECIAL_FOLDER}/${DIFF_FOLDER_NAME}/**`, diffVersions)
router.get('hyper://*/**', getFiles)
router.head('hyper://*/**', headFiles)

Expand Down Expand Up @@ -746,6 +748,54 @@ export default async function makeHyperFetch ({
return serveFile(drive, path, isRanged)
}

async function diffVersions (request) {
const url = new URL(request.url)
const { hostname, pathname: rawPathname } = url
const pathname = decodeURI(ensureLeadingSlash(rawPathname))

const parts = pathname.split('/')
const versions = parts[3]
// newVersion is undefined when versions is just a single number.
const [oldVersion, newVersion] = versions.split('..').map(Number)
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))

const drive = await getDrive(`hyper://${hostname}/`, true)

const resHeaders = {
Link: `<${url.href}>; rel="canonical"`
}

if (oldVersion > drive.version || (newVersion !== undefined && newVersion > drive.version)) {
return {
status: 404,
body: 'Versions out of range',
headers: {
...resHeaders,
ETag: drive.version
}
}
}

const snapshot = newVersion ? await drive.checkout(newVersion) : drive

const diffs = []

for await (const diff of snapshot.diff(oldVersion, realPath)) {
// TODO: Use try/catch?
diffs.push(diff)
}

return {
status: 200,
// TODO: More headers?
headers: {
...resHeaders,
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
},
body: JSON.stringify(diffs, null, '\t')
}
}

return fetch
}

Expand Down
46 changes: 46 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,52 @@ test('Check hyperdrive writability', async (t) => {
t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
})

test('Diff two directories', async (t) => {
const created = await nextURL(t)

const putURL = new URL('/example.txt', created)
const uploadedResponse = await fetch(putURL, {
method: 'put',
body: SAMPLE_CONTENT
})
await checkResponse(uploadedResponse, t)

const diffURL = new URL('/$/diff/1..2/', created)
const diffResponse = await fetch(diffURL)
await checkResponse(diffResponse, t)

const [{ left, right }] = await diffResponse.json()
const { seq, key, value } = left

t.equal(seq, 1, 'Left half of diff contained correct seq number')
t.equal(key, '/example.txt', 'Left half of diff contained correct key')
t.equal(value.blob.byteLength, 11, 'Blob for left half of diff contained correct byteLength')
t.ok(value.metadata.mtime, 'Left half of diff contained metadata mtime')
t.notOk(right, 'Diff contained null "right"')

// Test diff with only one old version specified
const onlyOneVersionDiffURL = new URL('/$/diff/1..2/', created)
const onlyOneVersionDiffResponse = await fetch(onlyOneVersionDiffURL)
await checkResponse(onlyOneVersionDiffResponse, t)

// Should be the same as the diff with new version explicitly set to the latest version
const [{ left: onlyOneVersionLeft, right: onlyOneVersionRight }] = await onlyOneVersionDiffResponse.json()
t.deepEqual([left, right], [onlyOneVersionLeft, onlyOneVersionRight], 'Got expected diff response with only one version')

// Test out-of-range diff error handling
const outOfRangeDiffURL = new URL('/$/diff/1..3/', created)
const outOfRangeDiffResponse = await fetch(outOfRangeDiffURL)
t.notOk(outOfRangeDiffResponse.ok, 'error response for out-of-range diff')

const outOfRangeDiffResponseEtagHeader = outOfRangeDiffResponse.headers.get('ETag')
t.equal(outOfRangeDiffResponseEtagHeader, '2', 'Headers got ETag for latest version')

// Test out-of-range diff error handling with only one version
const outOfRangeOnlyOneVersionDiffURL = new URL('/$/diff/3/', created)
const outOfRangeOnlyOneVersionDiffResponse = await fetch(outOfRangeOnlyOneVersionDiffURL)
t.notOk(outOfRangeOnlyOneVersionDiffResponse.ok, 'error response for out-of-range diff with only one version specified')
})

async function checkResponse (response, t, successMessage = 'Response OK') {
if (!response.ok) {
const message = await response.text()
Expand Down
Loading