From ca703fd5de303d2101fe2b2a5c0e3037b7507156 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Wed, 13 Aug 2025 11:33:51 +0100 Subject: Implement path-based downloads with blob redirect system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add fctdrive stat command to get blob IDs from paths - Add Drive_stat function to drive_server.ts for backend integration - Create /download/[...path] endpoint that redirects to /blob/{blobId} - Update UI file links to use /download/ paths instead of direct blob links - Update permalinks to use immutable /blob/{blobId} URLs - Fix host preservation in redirects for remote access 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/download/[...path]/route.ts | 37 ++++++++++++++++++++++ frontend/components/drive/DriveDirectoryClient.tsx | 18 ++++++++--- frontend/lib/drive_server.ts | 12 +++++++ src/main.rs | 20 ++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 frontend/app/download/[...path]/route.ts diff --git a/frontend/app/download/[...path]/route.ts b/frontend/app/download/[...path]/route.ts new file mode 100644 index 0000000..966a89e --- /dev/null +++ b/frontend/app/download/[...path]/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server' +import { Drive_stat } from '@/lib/drive_server' + +// GET /download/[...path] - Download file by path (redirects to blob endpoint) +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + try { + const { path } = await params + + // Reconstruct the full path + const fullPath = '/' + path.join('/') + + // Get filename from path for the download + const filename = path[path.length - 1] || 'download' + + // Get blob ID using Drive_stat + const blobId = await Drive_stat(fullPath) + + // Redirect to blob endpoint with filename - preserve original host + // Use X-Forwarded-Host or Host header to get the correct host + const forwardedHost = request.headers.get('x-forwarded-host') + const host = forwardedHost || request.headers.get('host') || new URL(request.url).host + const protocol = request.headers.get('x-forwarded-proto') || (new URL(request.url).protocol.replace(':', '')) + const redirectUrl = `${protocol}://${host}/blob/${blobId}?filename=${encodeURIComponent(filename)}` + + return NextResponse.redirect(redirectUrl) + + } catch (error) { + console.error('Download error:', error) + return new NextResponse( + error instanceof Error ? error.message : 'File not found', + { status: 404 } + ) + } +} \ No newline at end of file diff --git a/frontend/components/drive/DriveDirectoryClient.tsx b/frontend/components/drive/DriveDirectoryClient.tsx index 6089ec2..c405341 100644 --- a/frontend/components/drive/DriveDirectoryClient.tsx +++ b/frontend/components/drive/DriveDirectoryClient.tsx @@ -125,11 +125,21 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector } const copyPermalink = (item: DriveLsEntry) => { - const permalink = `${window.location.origin}/drive/file/${item.path}` + if (!item.blob) { + toast({ + title: "Cannot copy permalink", + description: "This item does not have a blob ID", + variant: "destructive" + }) + return + } + + const filename = item.path.split('/').pop() || 'download' + const permalink = `${window.location.origin}/blob/${item.blob}?filename=${encodeURIComponent(filename)}` navigator.clipboard.writeText(permalink).then(() => { toast({ - title: "Link copied!", - description: "Permalink has been copied to clipboard", + title: "Permalink copied!", + description: "Permanent blob link has been copied to clipboard", }) }) } @@ -531,7 +541,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector
{file.blob ? ( { + const result = spawnSync('fctdrive', ['stat', path], { encoding: 'utf-8' }); + if (result.error) { + throw new Error(`Failed to execute fctdrive: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); + } + return result.stdout.trim(); +} + /// builds a filesystem tree from Drive_ls entries export async function Drive_tree(): Promise { const entries = await Drive_ls('/', true); diff --git a/src/main.rs b/src/main.rs index 6431a88..ebbc817 100644 --- a/src/main.rs +++ b/src/main.rs @@ -747,6 +747,7 @@ enum Cmd { Import(ImportArgs), Blob(BlobArgs), Log(LogArgs), + Stat(StatArgs), } #[derive(Debug, Args)] @@ -847,6 +848,14 @@ struct LogArgs { common: CliCommon, } +#[derive(Debug, Args)] +struct StatArgs { + #[clap(flatten)] + common: CliCommon, + + path: DrivePath, +} + fn main() { let cli = Cli::parse(); @@ -858,6 +867,7 @@ fn main() { Cmd::Import(args) => cmd_import(args), Cmd::Blob(args) => cmd_blob(args), Cmd::Log(args) => cmd_log(args), + Cmd::Stat(args) => cmd_stat(args), } } @@ -1111,6 +1121,16 @@ fn cmd_log(args: LogArgs) { } } +fn cmd_stat(args: StatArgs) { + let ops = common_read_log_file(&args.common); + let mut fs = Fs::default(); + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + + let node_id = find_node(&fs, &args.path).unwrap(); + let node = &fs.nodes[node_id]; + println!("{}", node.blob); +} + fn collect_all_file_paths(root: &Path) -> Vec { let mut queue = vec![root.to_path_buf()]; let mut files = vec![]; -- cgit