diff options
| -rw-r--r-- | frontend/app/download/[...path]/route.ts | 37 | ||||
| -rw-r--r-- | frontend/components/drive/DriveDirectoryClient.tsx | 18 | ||||
| -rw-r--r-- | frontend/lib/drive_server.ts | 12 | ||||
| -rw-r--r-- | src/main.rs | 20 |
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 @@ | |||
| 1 | import { NextRequest, NextResponse } from 'next/server' | ||
| 2 | import { Drive_stat } from '@/lib/drive_server' | ||
| 3 | |||
| 4 | // GET /download/[...path] - Download file by path (redirects to blob endpoint) | ||
| 5 | export 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 | ||
| 112 | export 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 |
| 112 | export async function Drive_tree(): Promise<DriveTreeResponse> { | 124 | export 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)] | ||
| 852 | struct StatArgs { | ||
| 853 | #[clap(flatten)] | ||
| 854 | common: CliCommon, | ||
| 855 | |||
| 856 | path: DrivePath, | ||
| 857 | } | ||
| 858 | |||
| 850 | fn main() { | 859 | fn 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 | ||
| 1124 | fn 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 | |||
| 1114 | fn collect_all_file_paths(root: &Path) -> Vec<PathBuf> { | 1134 | fn 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![]; |
