feat(api): add HTTP Range request support for streaming#82
feat(api): add HTTP Range request support for streaming#82
Conversation
This leads to problems serializing TOML, see also: status-im/nim-serialization#104
Implements directory manifests for organizing multiple files into a tree:
- New DirectoryCodec (0xCD04) for directory manifest CIDs
- DirectoryManifest type with entries, totalSize, filesCount
- Two-phase upload: files uploaded individually, then /directory endpoint
finalizes them into a tree structure with POST JSON body
- HTML directory browsing with Archivist branding at /data/{cid}
- JSON directory listing when Accept header doesn't include text/html
- Path resolution within directories via /data/{cid}/path?p=subdir/file
- Auto-promotion of single-child root directories
API changes:
- POST /api/archivist/v1/directory - finalize directory from uploaded files
- GET /api/archivist/v1/data/{cid} - now serves HTML for directories
- Added MIME types: audio/mpeg, audio/flac, video/webm, etc.
- Relaxed filename validation to allow paths like "Album/track.mp3"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add HEAD endpoint for /api/archivist/v1/data/{cid} to support
metadata preflight checks (content-type, size) without full download
- Add HEAD endpoint for /api/archivist/v1/data/{cid}/network/stream
to support jsmediatags ID3 tag reading
- Fix directory traversal check to allow ".." in filenames (e.g.
"F.R.E.S.H..mp3") while still blocking actual traversal attacks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements byte-range requests (RFC 7233) for file downloads: - Add RangeStream wrapper for partial content streaming - Parse Range header (e.g., "bytes=0-499", "bytes=500-") - Return 206 Partial Content with Content-Range header - Return 416 Range Not Satisfiable for invalid ranges - Advertise Accept-Ranges: bytes on all file responses Enables seeking in media players for audio/video content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit
nph
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 554 to 556 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 559 to 564 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 568 to 574 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 577 to 583 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 586 to 589 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 644 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 656 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 672 to 673 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 700 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 729 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 866 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 870 to 872 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Lines 896 to 898 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 916 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/api.nim
Line 925 in 8a080d1
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
archivist-node/archivist/rest/directoryhtml.nim
Lines 463 to 465 in 8a080d1
[nph] reported by reviewdog 🐶
[nph] reported by reviewdog 🐶
archivist-node/archivist/streams/rangestream.nim
Lines 27 to 32 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/streams/rangestream.nim
Lines 40 to 43 in 8a080d1
[nph] reported by reviewdog 🐶
archivist-node/archivist/streams/rangestream.nim
Lines 50 to 53 in 8a080d1
| ProtoBuffer, initProtoBuffer, getField, getRequiredRepeatedField, | ||
| write, finish, ProtoResult |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| ProtoBuffer, initProtoBuffer, getField, getRequiredRepeatedField, | |
| write, finish, ProtoResult | |
| ProtoBuffer, initProtoBuffer, getField, getRequiredRepeatedField, write, finish, | |
| ProtoResult |
| entries.add(DirectoryEntry( | ||
| name: entryName, | ||
| cid: entryCid, | ||
| size: size.NBytes, | ||
| isDirectory: isDir != 0, | ||
| mimetype: mimetype, | ||
| )) |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| entries.add(DirectoryEntry( | |
| name: entryName, | |
| cid: entryCid, | |
| size: size.NBytes, | |
| isDirectory: isDir != 0, | |
| mimetype: mimetype, | |
| )) | |
| entries.add( | |
| DirectoryEntry( | |
| name: entryName, | |
| cid: entryCid, | |
| size: size.NBytes, | |
| isDirectory: isDir != 0, | |
| mimetype: mimetype, | |
| ) | |
| ) |
| success DirectoryManifest( | ||
| entries: entries, | ||
| totalSize: totalSize.NBytes, | ||
| name: name, | ||
| ) |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| success DirectoryManifest( | |
| entries: entries, | |
| totalSize: totalSize.NBytes, | |
| name: name, | |
| ) | |
| success DirectoryManifest(entries: entries, totalSize: totalSize.NBytes, name: name) |
| cid = blk.cid, | ||
| entries = directory.entries.len, | ||
| totalSize = directory.totalSize |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| cid = blk.cid, | |
| entries = directory.entries.len, | |
| totalSize = directory.totalSize | |
| cid = blk.cid, entries = directory.entries.len, totalSize = directory.totalSize |
| cid*: Cid | ||
| size*: NBytes | ||
| isDirectory*: bool | ||
| mimetype*: string # Empty string = not set |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| mimetype*: string # Empty string = not set | |
| mimetype*: string # Empty string = not set |
| # Validate and normalize path | ||
| var normalPath = pathStr.replace("\\", "/") | ||
| while normalPath.len > 0 and normalPath[0] == '/': | ||
| normalPath = normalPath[1..^1] |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| normalPath = normalPath[1..^1] | |
| normalPath = normalPath[1 ..^ 1] |
| for part in pathParts: | ||
| if part == "..": | ||
| return RestApiResponse.error( | ||
| Http400, "Invalid path (directory traversal not allowed): " & pathStr, |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| Http400, "Invalid path (directory traversal not allowed): " & pathStr, | |
| Http400, | |
| "Invalid path (directory traversal not allowed): " & pathStr, |
| inputEntries.add(InputEntry( | ||
| path: normalPath, | ||
| cid: cidVal, | ||
| size: size, | ||
| mimetype: mimetypeOpt, | ||
| )) |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| inputEntries.add(InputEntry( | |
| path: normalPath, | |
| cid: cidVal, | |
| size: size, | |
| mimetype: mimetypeOpt, | |
| )) | |
| inputEntries.add( | |
| InputEntry(path: normalPath, cid: cidVal, size: size, mimetype: mimetypeOpt) | |
| ) |
| current.subdirs[part] = DirNode( | ||
| name: part, | ||
| subdirs: initTable[string, DirNode](), | ||
| ) |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| current.subdirs[part] = DirNode( | |
| name: part, | |
| subdirs: initTable[string, DirNode](), | |
| ) | |
| current.subdirs[part] = | |
| DirNode(name: part, subdirs: initTable[string, DirNode]()) |
| current.files.add(( | ||
| name: pathParts[^1], | ||
| cid: entry.cid, | ||
| size: entry.size, | ||
| mimetype: entry.mimetype, | ||
| )) |
There was a problem hiding this comment.
[nph] reported by reviewdog 🐶
| current.files.add(( | |
| name: pathParts[^1], | |
| cid: entry.cid, | |
| size: entry.size, | |
| mimetype: entry.mimetype, | |
| )) | |
| current.files.add( | |
| ( | |
| name: pathParts[^1], | |
| cid: entry.cid, | |
| size: entry.size, | |
| mimetype: entry.mimetype, | |
| ) | |
| ) |
Implements byte-range requests (RFC 7233) for file downloads:
- Add RangeStream wrapper for partial content streaming
- Parse Range header (e.g., "bytes=0-499", "bytes=500-")
- Return 206 Partial Content with Content-Range header
- Return 416 Range Not Satisfiable for invalid ranges
- Advertise Accept-Ranges: bytes on all file responses