From c5f32e3767f59713b88addc1088fa5523489b8e5 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Sun, 23 Nov 2025 11:00:38 +0200 Subject: [PATCH 1/3] feat: allow to upload file directry from the url query param --- custom/uploader.vue | 82 +++++++++++++++++++++++++++++++++++++++++++-- index.ts | 45 +++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/custom/uploader.vue b/custom/uploader.vue index 485500c..0b43146 100644 --- a/custom/uploader.vue +++ b/custom/uploader.vue @@ -103,6 +103,8 @@ const progress = ref(0); const uploaded = ref(false); const uploadedSize = ref(0); +const downloadFileUrl = ref(''); + watch(() => uploaded, (value) => { emit('update:emptiness', !value); }); @@ -118,9 +120,45 @@ function uploadGeneratedImage(imgBlob) { }); } -onMounted(() => { +onMounted(async () => { const previewColumnName = `previewUrl_${props.meta.pluginInstanceId}`; - if (props.record[previewColumnName]) { + + let queryValues; + try { + queryValues = JSON.parse(atob(route.query.values as string)); + } catch (e) { + queryValues = {}; + } + + if (queryValues[props.meta.pathColumnName]) { + + + downloadFileUrl.value = queryValues[props.meta.pathColumnName]; + + const resp = await callAdminForthApi({ + path: `/plugin/${props.meta.pluginInstanceId}/get-file-download-url`, + method: 'POST', + body: { + filePath: queryValues[props.meta.pathColumnName] + }, + }); + if (resp.error) { + adminforth.alert({ + message: t('Error getting file url'), + variant: 'danger' + }); + return; + } + const file = await downloadAsFile(resp.url); + if (!file) { + return; + } + onFileChange({ + target: { + files: [file], + }, + }); + } else if (props.record[previewColumnName]) { imgPreview.value = props.record[previewColumnName]; uploaded.value = true; emit('update:emptiness', false); @@ -300,6 +338,46 @@ const onFileChange = async (e) => { } } +async function downloadAsFile(url) { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fileDownloadURL: url }), + }; + const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/plugin/${props.meta.pluginInstanceId}/proxy-download`; + const resp = await fetch(fullPath, options); + let isError = false; + try { + const jsonResp = await resp.clone().json(); + if (jsonResp.error) { + adminforth.alert({ + message: t('Error uploading file'), + variant: 'danger' + }); + isError = true; + } + } catch (e) { + + } + if (isError) { + return null; + } + + const blob = await resp.blob(); + + const filename = url.split('/').pop()?.split('?')[0] || `file`; + const filenameParts = filename.split('.'); + const extension = filenameParts.length > 1 ? filenameParts.pop() : ''; + const nameWithoutExt = filenameParts.join('.'); + const newFileName = extension + ? `${nameWithoutExt}_copy_${Date.now()}.${extension}` + : `${filename}_copy_${Date.now()}`; + + const file = new File([blob], newFileName, { type: blob.type }); + return file; +} \ No newline at end of file diff --git a/index.ts b/index.ts index 4c9e410..0cac7d3 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import { PluginOptions } from './types.js'; import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth"; import { Readable } from "stream"; import { RateLimiter } from "adminforth"; +import { url } from 'inspector/promises'; const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup'; @@ -86,6 +87,7 @@ export default class UploadPlugin extends AdminForthPlugin { minShowWidth: this.options.preview?.minShowWidth, generationPrompt: this.options.generation?.generationPrompt, recorPkFieldName: this.resourceConfig.columns.find((column: any) => column.primaryKey)?.name, + pathColumnName: this.options.pathColumnName, }; // define components which will be imported from other components this.componentPath('imageGenerator.vue'); @@ -411,6 +413,49 @@ export default class UploadPlugin extends AdminForthPlugin { }, }); + server.endpoint({ + method: 'POST', + path: `/plugin/${this.pluginInstanceId}/get-file-download-url`, + handler: async ({ body, adminUser }) => { + const { filePath } = body; + + const url = await this.options.storageAdapter.getDownloadUrl(filePath, 1800); + + return { + url, + }; + }, + }); + + server.endpoint({ + method: 'POST', + path: `/plugin/${this.pluginInstanceId}/proxy-download`, + handler: async ({ body, response }) => { + const { fileDownloadURL } = body; + + if (!fileDownloadURL) { + return { error: 'Missing fileDownloadURL' }; + } + + const upstream = await fetch(fileDownloadURL); + if (!upstream.ok || !upstream.body) { + return { error: `Failed to download file (status ${upstream.status})` }; + } + + const contentType = upstream.headers.get('content-type') || 'application/octet-stream'; + const contentLength = upstream.headers.get('content-length'); + const contentDisposition = upstream.headers.get('content-disposition'); + + response.setHeader('Content-Type', contentType); + if (contentLength) response.setHeader('Content-Length', contentLength); + if (contentDisposition) response.setHeader('Content-Disposition', contentDisposition); + + //@ts-ignore Node 18+: convert Web stream to Node stream and pipe to response + Readable.fromWeb(upstream.body).pipe(response.blobStream()); + return null; + }, + }); + } } \ No newline at end of file From 037766ea964e5fe8bd2c9bf6ed49b5f7460bc3b6 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Sun, 23 Nov 2025 11:04:32 +0200 Subject: [PATCH 2/3] fix: handle missing filePath in get-file-download-url handler --- index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 0cac7d3..82ead2a 100644 --- a/index.ts +++ b/index.ts @@ -418,7 +418,9 @@ export default class UploadPlugin extends AdminForthPlugin { path: `/plugin/${this.pluginInstanceId}/get-file-download-url`, handler: async ({ body, adminUser }) => { const { filePath } = body; - + if (!filePath) { + return { error: 'Missing filePath' }; + } const url = await this.options.storageAdapter.getDownloadUrl(filePath, 1800); return { From 47d4366def1ce068783ced2662898067f93e66e5 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 24 Nov 2025 08:49:31 +0200 Subject: [PATCH 3/3] fix: validate fileDownloadURL format in file download handler --- index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 82ead2a..43ca1c2 100644 --- a/index.ts +++ b/index.ts @@ -3,7 +3,6 @@ import { PluginOptions } from './types.js'; import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth"; import { Readable } from "stream"; import { RateLimiter } from "adminforth"; -import { url } from 'inspector/promises'; const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup'; @@ -439,6 +438,10 @@ export default class UploadPlugin extends AdminForthPlugin { return { error: 'Missing fileDownloadURL' }; } + if (!fileDownloadURL.startsWith(`http://${(this.options.storageAdapter as any).options.bucket}`) && !fileDownloadURL.startsWith(`https://${(this.options.storageAdapter as any).options.bucket}`)) { + return { error: 'Invalid fileDownloadURL ' }; + } + const upstream = await fetch(fileDownloadURL); if (!upstream.ok || !upstream.body) { return { error: `Failed to download file (status ${upstream.status})` };