summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-13 11:33:51 +0100
committerdiogo464 <[email protected]>2025-08-13 11:33:51 +0100
commitca703fd5de303d2101fe2b2a5c0e3037b7507156 (patch)
tree7078378ea380b1cfdf502aa62f2e1ada5708ccac
parent74069a896a3b831a19baefc0d9487060b34760b3 (diff)
Implement path-based downloads with blob redirect system
- 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 <[email protected]>
-rw-r--r--frontend/app/download/[...path]/route.ts37
-rw-r--r--frontend/components/drive/DriveDirectoryClient.tsx18
-rw-r--r--frontend/lib/drive_server.ts12
-rw-r--r--src/main.rs20
4 files changed, 83 insertions, 4 deletions
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 @@
1import { NextRequest, NextResponse } from 'next/server'
2import { Drive_stat } from '@/lib/drive_server'
3
4// GET /download/[...path] - Download file by path (redirects to blob endpoint)
5export async function GET(
6 request: NextRequest,
7 { params }: { params: Promise<{ path: string[] }> }
8) {
9 try {
10 const { path } = await params
11
12 // Reconstruct the full path
13 const fullPath = '/' + path.join('/')
14
15 // Get filename from path for the download
16 const filename = path[path.length - 1] || 'download'
17
18 // Get blob ID using Drive_stat
19 const blobId = await Drive_stat(fullPath)
20
21 // Redirect to blob endpoint with filename - preserve original host
22 // Use X-Forwarded-Host or Host header to get the correct host
23 const forwardedHost = request.headers.get('x-forwarded-host')
24 const host = forwardedHost || request.headers.get('host') || new URL(request.url).host
25 const protocol = request.headers.get('x-forwarded-proto') || (new URL(request.url).protocol.replace(':', ''))
26 const redirectUrl = `${protocol}://${host}/blob/${blobId}?filename=${encodeURIComponent(filename)}`
27
28 return NextResponse.redirect(redirectUrl)
29
30 } catch (error) {
31 console.error('Download error:', error)
32 return new NextResponse(
33 error instanceof Error ? error.message : 'File not found',
34 { status: 404 }
35 )
36 }
37} \ 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
125 } 125 }
126 126
127 const copyPermalink = (item: DriveLsEntry) => { 127 const copyPermalink = (item: DriveLsEntry) => {
128 const permalink = `${window.location.origin}/drive/file/${item.path}` 128 if (!item.blob) {
129 toast({
130 title: "Cannot copy permalink",
131 description: "This item does not have a blob ID",
132 variant: "destructive"
133 })
134 return
135 }
136
137 const filename = item.path.split('/').pop() || 'download'
138 const permalink = `${window.location.origin}/blob/${item.blob}?filename=${encodeURIComponent(filename)}`
129 navigator.clipboard.writeText(permalink).then(() => { 139 navigator.clipboard.writeText(permalink).then(() => {
130 toast({ 140 toast({
131 title: "Link copied!", 141 title: "Permalink copied!",
132 description: "Permalink has been copied to clipboard", 142 description: "Permanent blob link has been copied to clipboard",
133 }) 143 })
134 }) 144 })
135 } 145 }
@@ -531,7 +541,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector
531 <div className="min-w-0 max-w-[60vw]"> 541 <div className="min-w-0 max-w-[60vw]">
532 {file.blob ? ( 542 {file.blob ? (
533 <a 543 <a
534 href={`/blob/${file.blob}?filename=${encodeURIComponent(fileName)}`} 544 href={`/download${file.path}`}
535 className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer block truncate" 545 className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer block truncate"
536 target="_blank" 546 target="_blank"
537 rel="noopener noreferrer" 547 rel="noopener noreferrer"
diff --git a/frontend/lib/drive_server.ts b/frontend/lib/drive_server.ts
index 2a94fe9..41b9c71 100644
--- a/frontend/lib/drive_server.ts
+++ b/frontend/lib/drive_server.ts
@@ -108,6 +108,18 @@ export async function Drive_rename(oldPath: string, newPath: string, email: stri
108 } 108 }
109} 109}
110 110
111/// gets the blob ID for a given path
112export async function Drive_stat(path: string): Promise<string> {
113 const result = spawnSync('fctdrive', ['stat', path], { encoding: 'utf-8' });
114 if (result.error) {
115 throw new Error(`Failed to execute fctdrive: ${result.error.message}`);
116 }
117 if (result.status !== 0) {
118 throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`);
119 }
120 return result.stdout.trim();
121}
122
111/// builds a filesystem tree from Drive_ls entries 123/// builds a filesystem tree from Drive_ls entries
112export async function Drive_tree(): Promise<DriveTreeResponse> { 124export async function Drive_tree(): Promise<DriveTreeResponse> {
113 const entries = await Drive_ls('/', true); 125 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 {
747 Import(ImportArgs), 747 Import(ImportArgs),
748 Blob(BlobArgs), 748 Blob(BlobArgs),
749 Log(LogArgs), 749 Log(LogArgs),
750 Stat(StatArgs),
750} 751}
751 752
752#[derive(Debug, Args)] 753#[derive(Debug, Args)]
@@ -847,6 +848,14 @@ struct LogArgs {
847 common: CliCommon, 848 common: CliCommon,
848} 849}
849 850
851#[derive(Debug, Args)]
852struct StatArgs {
853 #[clap(flatten)]
854 common: CliCommon,
855
856 path: DrivePath,
857}
858
850fn main() { 859fn main() {
851 let cli = Cli::parse(); 860 let cli = Cli::parse();
852 861
@@ -858,6 +867,7 @@ fn main() {
858 Cmd::Import(args) => cmd_import(args), 867 Cmd::Import(args) => cmd_import(args),
859 Cmd::Blob(args) => cmd_blob(args), 868 Cmd::Blob(args) => cmd_blob(args),
860 Cmd::Log(args) => cmd_log(args), 869 Cmd::Log(args) => cmd_log(args),
870 Cmd::Stat(args) => cmd_stat(args),
861 } 871 }
862} 872}
863 873
@@ -1111,6 +1121,16 @@ fn cmd_log(args: LogArgs) {
1111 } 1121 }
1112} 1122}
1113 1123
1124fn cmd_stat(args: StatArgs) {
1125 let ops = common_read_log_file(&args.common);
1126 let mut fs = Fs::default();
1127 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
1128
1129 let node_id = find_node(&fs, &args.path).unwrap();
1130 let node = &fs.nodes[node_id];
1131 println!("{}", node.blob);
1132}
1133
1114fn collect_all_file_paths(root: &Path) -> Vec<PathBuf> { 1134fn collect_all_file_paths(root: &Path) -> Vec<PathBuf> {
1115 let mut queue = vec![root.to_path_buf()]; 1135 let mut queue = vec![root.to_path_buf()];
1116 let mut files = vec![]; 1136 let mut files = vec![];