From 394711b83ff10ea44082c639f07bf5bee52b1f9e Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 4 May 2026 20:27:20 +0530 Subject: [PATCH 1/2] fix: correct deployment upload queue state --- src/lib/components/uploadBox.svelte | 148 +++++++++- src/lib/helpers/files.ts | 23 ++ src/lib/stores/uploader.ts | 275 +++++++++++------- .../create-function/manual/+page.svelte | 27 +- .../(modals)/createManual.svelte | 17 +- .../sites/create-site/manual/+page.svelte | 40 +-- .../createManualDeploymentModal.svelte | 17 +- .../storage/bucket-[bucket]/+page.svelte | 5 +- 8 files changed, 399 insertions(+), 153 deletions(-) diff --git a/src/lib/components/uploadBox.svelte b/src/lib/components/uploadBox.svelte index 4b7d3aedf9..e1803f19c7 100644 --- a/src/lib/components/uploadBox.svelte +++ b/src/lib/components/uploadBox.svelte @@ -1,17 +1,139 @@ -{#if $uploader?.isOpen} - { - return { - name: file.name, - size: file.size, - status: file.status, - progress: file.progress - }; - })} - on:close={() => uploader.close()} /> +{#if $uploader.isOpen} +
+ {#each groups as group (group.key)} +
+
+

+ + {group.title} ({group.files.length}) + +

+ + +
+ +
+
    + {#each group.files as file (file.clientId)} + {@const readableSize = humanFileSize(file.size)} +
  • + +
    + {file.name} + + {readableSize.value + readableSize.unit} + +
    + +
  • + {/each} +
+
+
+ {/each} +
{/if} + + diff --git a/src/lib/helpers/files.ts b/src/lib/helpers/files.ts index de236f5f28..6aaf487695 100644 --- a/src/lib/helpers/files.ts +++ b/src/lib/helpers/files.ts @@ -80,6 +80,29 @@ export async function gzipUpload(files: FileList) { return uploadFile; } +export function isTarGzFile(file: File) { + return file.name.toLowerCase().endsWith('.tar.gz'); +} + +export function getInvalidDeploymentArchiveReason( + files: FileList | File[] | null | undefined, + maxSize?: number +) { + if (!files) return null; + + const normalizedFiles = Array.from(files); + + if (!normalizedFiles.length) return null; + if (maxSize && normalizedFiles.some((file) => file.size > maxSize)) { + return InvalidFileType.SIZE; + } + if (normalizedFiles.some((file) => !isTarGzFile(file))) { + return InvalidFileType.EXTENSION; + } + + return null; +} + export function removeFile(file: File, files: FileList) { const filteredFiles = Array.from(files).filter((f) => f.name !== file.name); const dataTransfer = new DataTransfer(); diff --git a/src/lib/stores/uploader.ts b/src/lib/stores/uploader.ts index 97f475f992..bf0125076e 100644 --- a/src/lib/stores/uploader.ts +++ b/src/lib/stores/uploader.ts @@ -1,21 +1,34 @@ +import { page } from '$app/state'; +import { getApiEndpoint } from '$lib/stores/sdk'; import { Client, Functions, ID, type Models, Sites, Storage } from '@appwrite.io/console'; import { writable } from 'svelte/store'; -import { getApiEndpoint } from '$lib/stores/sdk'; -import { page } from '$app/state'; -type UploaderFile = { +export type UploadKind = 'storage' | 'site-deployment' | 'function-deployment'; +export type UploadGroupKey = 'storage' | 'deployments'; + +export type UploaderFile = { $id: string; + clientId: string; resourceId: string; name: string; progress: number; size: number; status: 'failed' | 'pending' | 'success'; + kind: UploadKind; error?: string; }; + +export type UploadGroupState = { + isOpen: boolean; + isVisible: boolean; +}; + +type UploadGroups = Record; + export type Uploader = { isOpen: boolean; - isCollapsed: boolean; files: UploaderFile[]; + groups: UploadGroups; }; const temporaryStorage = (region: string, projectId: string) => { @@ -41,22 +54,76 @@ const temporaryFunctions = (region: string, projectId: string) => { const MAX_CONCURRENT_UPLOADS = 5; +const createInitialGroups = (): UploadGroups => ({ + storage: { + isOpen: true, + isVisible: true + }, + deployments: { + isOpen: true, + isVisible: true + } +}); + +export function getUploadGroupKey(kind: UploadKind): UploadGroupKey { + return kind === 'storage' ? 'storage' : 'deployments'; +} + +function hasFilesForGroup(files: UploaderFile[], groupKey: UploadGroupKey) { + return files.some((file) => getUploadGroupKey(file.kind) === groupKey); +} + +function refreshUploaderState(state: Uploader) { + for (const groupKey of Object.keys(state.groups) as UploadGroupKey[]) { + if (!hasFilesForGroup(state.files, groupKey)) { + state.groups[groupKey] = { + ...createInitialGroups()[groupKey] + }; + } + } + + state.isOpen = (Object.keys(state.groups) as UploadGroupKey[]).some( + (groupKey) => hasFilesForGroup(state.files, groupKey) && state.groups[groupKey].isVisible + ); + + return state; +} + const createUploader = () => { const { subscribe, set, update } = writable({ isOpen: false, - isCollapsed: false, - files: [] + files: [], + groups: createInitialGroups() }); - const updateFile = (id: string, file: Partial) => { - return update((n) => { - const index = n.files.findIndex((f) => f.$id === id); - n.files[index] = { - ...n.files[index], + const updateFile = (clientId: string, file: Partial) => { + return update((state) => { + const index = state.files.findIndex((item) => item.clientId === clientId); + + if (index === -1) { + return state; + } + + state.files[index] = { + ...state.files[index], ...file }; - return n; + return state; + }); + }; + + const addFileToQueue = (file: UploaderFile) => { + update((state) => { + const groupKey = getUploadGroupKey(file.kind); + + state.files.unshift(file); + state.groups[groupKey] = { + isOpen: true, + isVisible: true + }; + + return refreshUploaderState(state); }); }; @@ -70,18 +137,17 @@ const createUploader = () => { ) => { const newFile: UploaderFile = { $id: id, + clientId: id, resourceId: bucketId, name: file.name, size: file.size, progress: 0, - status: 'pending' + status: 'pending', + kind: 'storage' }; - update((n) => { - n.isOpen = true; - n.isCollapsed = false; - n.files.unshift(newFile); - return n; - }); + + addFileToQueue(newFile); + try { const uploadedFile = await temporaryStorage(region, projectId).createFile({ bucketId, @@ -92,17 +158,17 @@ const createUploader = () => { newFile.$id = progress.$id; newFile.progress = progress.progress; newFile.status = progress.progress === 100 ? 'success' : 'pending'; - updateFile(progress.$id, newFile); + updateFile(newFile.clientId, newFile); } }); newFile.$id = uploadedFile.$id; newFile.progress = 100; newFile.status = 'success'; - updateFile(newFile.$id, newFile); + updateFile(newFile.clientId, newFile); } catch (e) { newFile.status = 'failed'; newFile.error = e?.message ?? 'Upload failed'; - updateFile(newFile.$id, newFile); + updateFile(newFile.clientId, newFile); throw e; } }; @@ -139,25 +205,47 @@ const createUploader = () => { return results; }; - return { - subscribe, + const uploadDeployment = async ( + kind: Extract, + resourceId: string, + name: string, + size: number, + request: () => Promise + ) => { + const newDeployment: UploaderFile = { + $id: '', + clientId: ID.unique(), + resourceId, + name, + size, + progress: 0, + status: 'pending', + kind + }; - close: () => - update((n) => { - n.isOpen = !n.isOpen; - return n; - }), - toggle: () => - update((n) => { - n.isCollapsed = !n.isCollapsed; + addFileToQueue(newDeployment); - return n; - }), + try { + const uploadedFile = await request(); + newDeployment.$id = uploadedFile.$id; + newDeployment.progress = 100; + newDeployment.status = 'success'; + updateFile(newDeployment.clientId, newDeployment); + } catch (e) { + newDeployment.status = 'failed'; + newDeployment.error = e?.message ?? 'Upload failed'; + updateFile(newDeployment.clientId, newDeployment); + throw e; + } + }; + + return { + subscribe, reset: () => set({ isOpen: false, - isCollapsed: false, - files: [] + files: [], + groups: createInitialGroups() }), uploadFile, uploadFiles, @@ -166,53 +254,24 @@ const createUploader = () => { code, buildCommand, installCommand, - startCommand, outputDirectory }: { siteId: string; code: File; buildCommand?: string; installCommand?: string; - startCommand?: string; outputDirectory?: string; }) => { - const newDeployment: UploaderFile = { - $id: '', - resourceId: siteId, - name: code.name, - size: code.size, - progress: 0, - status: 'pending' - }; - update((n) => { - n.isOpen = true; - n.isCollapsed = false; - n.files.unshift(newDeployment); - return n; - }); - const deploymentPayload = { - siteId, - code, - activate: true, - buildCommand, - installCommand, - startCommand, - outputDirectory, - onProgress: (progress) => { - newDeployment.$id = progress.$id; - newDeployment.progress = progress.progress; - newDeployment.status = progress.progress === 100 ? 'success' : 'pending'; - updateFile(progress.$id, newDeployment); - } - }; - const uploadedFile = await temporarySites( - page.params.region, - page.params.project - ).createDeployment(deploymentPayload); - newDeployment.$id = uploadedFile.$id; - newDeployment.progress = 100; - newDeployment.status = 'success'; - updateFile(newDeployment.$id, newDeployment); + return uploadDeployment('site-deployment', siteId, code.name, code.size, async () => + temporarySites(page.params.region, page.params.project).createDeployment({ + siteId, + code, + activate: true, + buildCommand, + installCommand, + outputDirectory + }) + ); }, uploadFunctionDeployment: async ({ functionId, @@ -221,52 +280,42 @@ const createUploader = () => { functionId: string; code: File; }) => { - const newDeployment: UploaderFile = { - $id: '', - resourceId: functionId, - name: code.name, - size: code.size, - progress: 0, - status: 'pending' - }; - update((n) => { - n.isOpen = true; - n.isCollapsed = false; - n.files.unshift(newDeployment); - return n; - }); - const uploadedFile = await temporaryFunctions( - page.params.region, - page.params.project - ).createDeployment({ + return uploadDeployment( + 'function-deployment', functionId, - code, - activate: true, - onProgress: (progress) => { - newDeployment.$id = progress.$id; - newDeployment.progress = progress.progress; - newDeployment.status = progress.progress === 100 ? 'success' : 'pending'; - updateFile(progress.$id, newDeployment); - } + code.name, + code.size, + async () => + temporaryFunctions(page.params.region, page.params.project).createDeployment({ + functionId, + code, + activate: true + }) + ); + }, + toggleGroup: (groupKey: UploadGroupKey) => { + update((state) => { + state.groups[groupKey].isOpen = !state.groups[groupKey].isOpen; + return refreshUploaderState(state); + }); + }, + hideGroup: (groupKey: UploadGroupKey) => { + update((state) => { + state.groups[groupKey].isVisible = false; + return refreshUploaderState(state); }); - newDeployment.$id = uploadedFile.$id; - newDeployment.progress = 100; - newDeployment.status = 'success'; - updateFile(newDeployment.$id, newDeployment); }, removeFromQueue: (id: string) => { - update((n) => { - n.files = n.files.filter((f) => f.$id !== id); - n.isOpen = n.files.length !== 0; - return n; + update((state) => { + state.files = state.files.filter((file) => file.$id !== id && file.clientId !== id); + return refreshUploaderState(state); }); }, removeFile: async (file: Models.File) => { if (file.chunksTotal === file.chunksUploaded) { - return update((n) => { - n.files = n.files.filter((f) => f.$id !== file.$id); - - return n; + return update((state) => { + state.files = state.files.filter((item) => item.$id !== file.$id); + return refreshUploaderState(state); }); } } diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte index d0e10cdf4b..b7d3f5c5a4 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte @@ -19,7 +19,11 @@ import Configuration from './configuration.svelte'; import { getIconFromRuntime } from '$lib/stores/runtimes'; import { regionalConsoleVariables } from '../../../store'; - import { InvalidFileType, removeFile } from '$lib/helpers/files'; + import { + getInvalidDeploymentArchiveReason, + InvalidFileType, + removeFile + } from '$lib/helpers/files'; import { isCloud } from '$lib/system'; import { humanFileSize } from '$lib/helpers/sizeConvertion'; import { currentPlan } from '$lib/stores/organization'; @@ -108,7 +112,9 @@ }); await promise; - const upload = $uploader.files.find((f) => f.resourceId === func.$id); + const upload = $uploader.files.find( + (f) => f.resourceId === func.$id && f.kind === 'function-deployment' + ); if (upload?.status === 'success') { const deploymentId = upload.$id; @@ -126,9 +132,11 @@ invalidate(Dependencies.FUNCTION); } catch (e) { - const upload = $uploader.files.find((f) => f.resourceId === func?.$id); + const upload = $uploader.files.find( + (f) => f.resourceId === func?.$id && f.kind === 'function-deployment' + ); if (upload) { - uploader.removeFromQueue(upload.$id); + uploader.removeFromQueue(upload.clientId); } addNotification({ type: 'error', @@ -158,6 +166,15 @@ } } + $: if (files?.length) { + const reason = getInvalidDeploymentArchiveReason(files, maxSize); + + if (reason) { + files = undefined; + handleInvalid(new CustomEvent('invalid', { detail: { reason } })); + } + } + $: filesList = files?.length ? Array.from(files).map((f) => { return { @@ -198,7 +215,7 @@ diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte index 8250b14045..fcdd29db17 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte @@ -4,7 +4,11 @@ import { Modal } from '$lib/components'; import { Dependencies } from '$lib/constants'; import { Button } from '$lib/elements/forms'; - import { InvalidFileType, removeFile } from '$lib/helpers/files'; + import { + getInvalidDeploymentArchiveReason, + InvalidFileType, + removeFile + } from '$lib/helpers/files'; import { addNotification } from '$lib/stores/notifications'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import { Icon, Layout, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte'; @@ -60,6 +64,15 @@ } } + $: if (files?.length) { + const reason = getInvalidDeploymentArchiveReason(files, maxSize); + + if (reason) { + files = undefined; + handleInvalid(new CustomEvent('invalid', { detail: { reason } })); + } + } + $: filesList = files?.length ? Array.from(files).map((f) => { return { @@ -80,7 +93,7 @@ Upload a tar.gz file containing your function source code f.resourceId === site.$id); + const upload = $uploader.files.find( + (f) => f.resourceId === site.$id && f.kind === 'site-deployment' + ); if (upload?.status === 'success') { const deploymentId = upload.$id; @@ -129,9 +134,11 @@ await goto(`${resolvedPath}?site=${site.$id}&deployment=${deploymentId}`); } } catch (e) { - const upload = $uploader.files.find((f) => f.resourceId === site?.$id); + const upload = $uploader.files.find( + (f) => f.resourceId === site?.$id && f.kind === 'site-deployment' + ); if (upload) { - uploader.removeFromQueue(upload.$id); + uploader.removeFromQueue(upload.clientId); } addNotification({ type: 'error', @@ -145,17 +152,7 @@ let reason = e.detail?.reason ?? ''; if (!reason) { - const nativeEvent = e.detail as Event | undefined; - const input = (nativeEvent?.currentTarget ?? nativeEvent?.target) as - | HTMLInputElement - | undefined; - const pickedFiles = Array.from(input?.files ?? []); - - if (pickedFiles.some((file) => file.size > maxSize)) { - reason = InvalidFileType.SIZE; - } else if (pickedFiles.some((file) => !file.name.toLowerCase().endsWith('.tar.gz'))) { - reason = InvalidFileType.EXTENSION; - } + reason = getInvalidDeploymentArchiveReason(files, maxSize) ?? ''; } if (reason === InvalidFileType.EXTENSION) { @@ -176,6 +173,15 @@ } } + $: if (files?.length) { + const reason = getInvalidDeploymentArchiveReason(files, maxSize); + + if (reason) { + files = undefined; + handleInvalid(new CustomEvent('invalid', { detail: { reason } })); + } + } + $: filesList = files?.length ? Array.from(files).map((f) => { return { @@ -205,7 +211,7 @@ Upload a tar.gz containing your site source code { return { @@ -80,7 +93,7 @@ Upload a tar.gz file containing your site source code { isUploading = $uploader.files.some( (file) => - file.status !== 'success' && file.progress < 100 && file.status !== 'failed' + file.kind === 'storage' && + file.status !== 'success' && + file.progress < 100 && + file.status !== 'failed' ); }); }); From e424e6e1bb3e128d20a5a27e872be79cc06b22f3 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 5 May 2026 10:54:34 +0530 Subject: [PATCH 2/2] fix: restore deployment upload progress --- src/lib/helpers/files.ts | 8 ++------ src/lib/stores/uploader.ts | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/lib/helpers/files.ts b/src/lib/helpers/files.ts index 6aaf487695..1efd2319ef 100644 --- a/src/lib/helpers/files.ts +++ b/src/lib/helpers/files.ts @@ -80,10 +80,6 @@ export async function gzipUpload(files: FileList) { return uploadFile; } -export function isTarGzFile(file: File) { - return file.name.toLowerCase().endsWith('.tar.gz'); -} - export function getInvalidDeploymentArchiveReason( files: FileList | File[] | null | undefined, maxSize?: number @@ -93,10 +89,10 @@ export function getInvalidDeploymentArchiveReason( const normalizedFiles = Array.from(files); if (!normalizedFiles.length) return null; - if (maxSize && normalizedFiles.some((file) => file.size > maxSize)) { + if (maxSize !== undefined && normalizedFiles.some((file) => file.size > maxSize)) { return InvalidFileType.SIZE; } - if (normalizedFiles.some((file) => !isTarGzFile(file))) { + if (normalizedFiles.some((file) => !file.name.toLowerCase().endsWith('.tar.gz'))) { return InvalidFileType.EXTENSION; } diff --git a/src/lib/stores/uploader.ts b/src/lib/stores/uploader.ts index bf0125076e..9640326ff5 100644 --- a/src/lib/stores/uploader.ts +++ b/src/lib/stores/uploader.ts @@ -31,6 +31,11 @@ export type Uploader = { groups: UploadGroups; }; +type UploadProgress = { + $id: string; + progress: number; +}; + const temporaryStorage = (region: string, projectId: string) => { const clientProject = new Client().setMode('admin'); const endpoint = getApiEndpoint(region); @@ -210,7 +215,7 @@ const createUploader = () => { resourceId: string, name: string, size: number, - request: () => Promise + request: (onProgress: (progress: UploadProgress) => void) => Promise ) => { const newDeployment: UploaderFile = { $id: '', @@ -226,7 +231,12 @@ const createUploader = () => { addFileToQueue(newDeployment); try { - const uploadedFile = await request(); + const uploadedFile = await request((progress) => { + newDeployment.$id = progress.$id; + newDeployment.progress = progress.progress; + newDeployment.status = progress.progress === 100 ? 'success' : 'pending'; + updateFile(newDeployment.clientId, newDeployment); + }); newDeployment.$id = uploadedFile.$id; newDeployment.progress = 100; newDeployment.status = 'success'; @@ -262,13 +272,14 @@ const createUploader = () => { installCommand?: string; outputDirectory?: string; }) => { - return uploadDeployment('site-deployment', siteId, code.name, code.size, async () => + return uploadDeployment('site-deployment', siteId, code.name, code.size, (onProgress) => temporarySites(page.params.region, page.params.project).createDeployment({ siteId, code, activate: true, buildCommand, installCommand, + onProgress, outputDirectory }) ); @@ -285,11 +296,12 @@ const createUploader = () => { functionId, code.name, code.size, - async () => + (onProgress) => temporaryFunctions(page.params.region, page.params.project).createDeployment({ functionId, code, - activate: true + activate: true, + onProgress }) ); },