Skip to content

Commit b0ddfde

Browse files
committed
feat: storage uploadTask
1 parent 64a3cd2 commit b0ddfde

File tree

8 files changed

+202
-66
lines changed

8 files changed

+202
-66
lines changed

README.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,14 @@ Collections can also take a Firestore Query instead of a path:
279279
</Collection>
280280
```
281281

282-
### DownloadLink
282+
### DownloadURL
283283

284-
DownloadLink provides a `link` to download a file from Firebase Storage and its `reference`.
284+
DownloadURL provides a `link` to download a file from Firebase Storage and its `reference`.
285285

286286
```svelte
287-
<DownloadLink ref={item} let:link let:ref>
288-
<a href="{link}" download>Download {ref?.name}</a>
289-
</DownloadLink>
287+
<DownloadURL ref={item} let:link let:ref>
288+
<a href={link} download>Download {ref?.name}</a>
289+
</DownloadURL>
290290
```
291291

292292
### StorageList
@@ -318,9 +318,26 @@ StorageList provides a list of `items` and `prefixes` corresponding to the list
318318
</StorageList>
319319
```
320320

321-
You can combine
321+
### UploadTask
322322

323-
### Using Components Together
323+
Upload a file with progress tracking
324+
325+
```svelte
326+
<UploadTask ref="filename.txt" data={someBlob} let:progress let:snapshot>
327+
{#if snapshot?.state === "running"}
328+
{progress}% uploaded
329+
{/if}
330+
331+
{#if snapshot?.state === "success"}
332+
<DownloadURL ref={snapshot?.ref} let:link>
333+
<a href={link} download>Download</a>
334+
</DownloadURL>
335+
{/if}
336+
</UploadTask>
337+
```
338+
339+
340+
## Using Components Together
324341

325342
These components can be combined to build complex realtime apps. It's especially powerful when fetching data that requires the current user's UID or a related document's path.
326343

docs/src/pages/_alt.astro

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/lib/components/UploadTask.svelte

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script lang="ts">
2+
import { uploadTaskStore } from "$lib/stores/storage.js";
3+
import { getFirebaseContext } from "$lib/stores/sdk.js";
4+
import type {
5+
FirebaseStorage,
6+
UploadTask,
7+
StorageReference,
8+
UploadMetadata,
9+
UploadTaskSnapshot,
10+
} from "firebase/storage";
11+
12+
export let ref: string | StorageReference;
13+
export let data: Blob | Uint8Array | ArrayBuffer;
14+
export let metadata: UploadMetadata | undefined = undefined;
15+
16+
const { storage } = getFirebaseContext();
17+
const upload = uploadTaskStore(storage!, ref, data, metadata);
18+
19+
interface $$Slots {
20+
default: {
21+
task: UploadTask | undefined;
22+
ref: StorageReference | null;
23+
snapshot: UploadTaskSnapshot | null;
24+
progress: number;
25+
storage?: FirebaseStorage;
26+
};
27+
}
28+
29+
$: progress = ($upload?.bytesTransferred! / $upload?.totalBytes!) * 100 ?? 0;
30+
</script>
31+
32+
{#if $upload !== undefined}
33+
<slot task={$upload?.task} snapshot={$upload} {progress} ref={upload.reference} {storage} />
34+
{/if}

src/lib/index.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,29 @@ import Doc from './components/Doc.svelte';
55
import FirebaseApp from './components/FirebaseApp.svelte';
66
import SignedIn from './components/SignedIn.svelte';
77
import SignedOut from './components/SignedOut.svelte';
8+
import DownloadURL from './components/DownloadURL.svelte';
9+
import StorageList from './components/StorageList.svelte';
10+
import UploadTask from './components/UploadTask.svelte';
811
import { userStore } from './stores/auth.js';
912
import { docStore, collectionStore } from './stores/firestore.js';
1013
import { getFirebaseContext } from './stores/sdk.js';
11-
14+
import { downloadUrlStore, storageListStore, uploadTaskStore } from './stores/storage.js';
15+
1216
export {
1317
Doc,
1418
User,
1519
Collection,
1620
FirebaseApp,
1721
SignedOut,
1822
SignedIn,
23+
UploadTask,
24+
StorageList,
25+
DownloadURL,
26+
downloadUrlStore,
27+
storageListStore,
28+
uploadTaskStore,
1929
docStore,
2030
collectionStore,
2131
userStore,
22-
getFirebaseContext
32+
getFirebaseContext,
2333
}

src/lib/stores/storage.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { readable } from "svelte/store";
2-
import { getDownloadURL, list, ref } from "firebase/storage";
2+
import {
3+
getDownloadURL,
4+
list,
5+
ref,
6+
uploadBytesResumable,
7+
} from "firebase/storage";
38

4-
import type {
9+
import type {
510
StorageReference,
611
FirebaseStorage,
712
ListResult,
13+
UploadTaskSnapshot,
14+
UploadMetadata,
815
} from "firebase/storage";
916

1017
const defaultListResult: ListResult = {
@@ -28,7 +35,6 @@ export function storageListStore(
2835
reference: string | StorageReference,
2936
startWith: ListResult = defaultListResult
3037
): StorageListStore {
31-
3238
// Fallback for SSR
3339
if (!globalThis.window) {
3440
const { subscribe } = readable(startWith);
@@ -50,7 +56,8 @@ export function storageListStore(
5056
};
5157
}
5258

53-
const storageRef = typeof reference === "string" ? ref(storage, reference) : reference;
59+
const storageRef =
60+
typeof reference === "string" ? ref(storage, reference) : reference;
5461

5562
const { subscribe } = readable(startWith, (set) => {
5663
list(storageRef).then((snapshot) => {
@@ -80,7 +87,6 @@ export function downloadUrlStore(
8087
reference: string | StorageReference,
8188
startWith: string | null = null
8289
): DownloadUrlStore {
83-
8490
// Fallback for SSR
8591
if (!globalThis.window) {
8692
const { subscribe } = readable(startWith);
@@ -102,7 +108,8 @@ export function downloadUrlStore(
102108
};
103109
}
104110

105-
const storageRef = typeof reference === "string" ? ref(storage, reference) : reference;
111+
const storageRef =
112+
typeof reference === "string" ? ref(storage, reference) : reference;
106113

107114
const { subscribe } = readable(startWith, (set) => {
108115
getDownloadURL(storageRef).then((snapshot) => {
@@ -116,3 +123,65 @@ export function downloadUrlStore(
116123
};
117124
}
118125

126+
interface UploadTaskStore {
127+
subscribe: (
128+
cb: (value: UploadTaskSnapshot | null) => void
129+
) => void | (() => void);
130+
reference: StorageReference | null;
131+
}
132+
133+
export function uploadTaskStore(
134+
storage: FirebaseStorage,
135+
reference: string | StorageReference,
136+
data: Blob | Uint8Array | ArrayBuffer,
137+
metadata?: UploadMetadata | undefined
138+
): UploadTaskStore {
139+
// Fallback for SSR
140+
if (!globalThis.window) {
141+
const { subscribe } = readable(null);
142+
return {
143+
subscribe,
144+
reference: null,
145+
};
146+
}
147+
148+
// Fallback for missing SDK
149+
if (!storage) {
150+
console.warn(
151+
"Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?"
152+
);
153+
const { subscribe } = readable(null);
154+
return {
155+
subscribe,
156+
reference: null,
157+
};
158+
}
159+
160+
const storageRef =
161+
typeof reference === "string" ? ref(storage, reference) : reference;
162+
163+
let unsubscribe: () => void;
164+
165+
const { subscribe } = readable<UploadTaskSnapshot | null>(null, (set) => {
166+
const task = uploadBytesResumable(storageRef, data, metadata);
167+
unsubscribe = task.on(
168+
"state_changed",
169+
(snapshot) => {
170+
set(snapshot);
171+
},
172+
(error) => {
173+
console.error(error);
174+
set(task.snapshot);
175+
},
176+
() => {
177+
set(task.snapshot);
178+
}
179+
);
180+
return () => unsubscribe();
181+
});
182+
183+
return {
184+
subscribe,
185+
reference: storageRef,
186+
};
187+
}

src/routes/storage-test/+page.svelte

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,58 @@
11
<script lang="ts">
2-
import DownloadLink from "$lib/components/DownloadLink.svelte";
3-
import StorageList from "$lib/components/StorageList.svelte";
2+
import DownloadURL from "$lib/components/DownloadURL.svelte";
3+
import StorageList from "$lib/components/StorageList.svelte";
4+
import UploadTask from "$lib/components/UploadTask.svelte";
5+
6+
let file: any;
7+
8+
function makeFile() {
9+
file = new Blob(["test"], { type: "text/plain" });
10+
}
11+
12+
function chooseFile(e: any) {
13+
file = e.target.files[0];
14+
}
415
</script>
516

617
<h1>Storage Test</h1>
718

819
<StorageList ref="/" let:list>
9-
<ul>
10-
{#if list === null}
11-
<li>Loading...</li>
12-
{:else if list.prefixes.length === 0 && list.items.length === 0}
13-
<li>Empty</li>
14-
{:else}
15-
{#each list.items as item}
16-
<li>
17-
<DownloadLink ref={item} let:link let:ref>
18-
<a data-testid="download-link" href="{link}" download>{ref?.name}</a>
19-
</DownloadLink>
20-
</li>
21-
{/each}
22-
{/if}
23-
</ul>
20+
<ul>
21+
{#if list === null}
22+
<li>Loading...</li>
23+
{:else if list.prefixes.length === 0 && list.items.length === 0}
24+
<li>Empty</li>
25+
{:else}
26+
{#each list.items as item}
27+
<li>
28+
<DownloadURL ref={item} let:link let:ref>
29+
<a data-testid="download-link" href={link} download>{ref?.name}</a>
30+
</DownloadURL>
31+
</li>
32+
{/each}
33+
{/if}
34+
</ul>
2435
</StorageList>
36+
37+
<h2>Upload Task</h2>
38+
39+
<input type="file" on:change={chooseFile} />
40+
<button on:click={makeFile}>Make File</button>
41+
42+
{#if file}
43+
<UploadTask ref="test-upload.txt" data={file} let:progress let:snapshot>
44+
{#if snapshot?.state === "running" || snapshot?.state === "success"}
45+
<p data-testid="progress">{progress}% uploaded</p>
46+
{/if}
47+
48+
{#if snapshot?.state === "error"}
49+
Upload failed
50+
{/if}
51+
52+
{#if snapshot?.state === "success"}
53+
<DownloadURL ref={snapshot?.ref} let:link let:ref>
54+
<a data-testid="download-link2" href={link} download> {ref?.name} </a>
55+
</DownloadURL>
56+
{/if}
57+
</UploadTask>
58+
{/if}

tests/storage.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ test('Renders download links', async ({ page }) => {
88
expect( linksCount ).toBeGreaterThan(0);
99
});
1010

11+
test.only('Uploads a file', async ({ page }) => {
12+
await page.goto('/storage-test');
13+
await page.getByRole('button', { name: 'Make File' }).click();
14+
await expect(page.getByTestId('progress')).toContainText('100% uploaded');
15+
await expect(page.getByTestId('download-link2')).toContainText('test-upload.txt');
16+
});

0 commit comments

Comments
 (0)