Skip to content

Commit 414f09c

Browse files
SvelteKit file uploads (sveltejs#8)
One example for file uploads with NodeJS, one with S3 --------- Co-authored-by: Simon Holthausen <[email protected]>
1 parent 3598a5f commit 414f09c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3546
-63
lines changed

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1-
node_modules
1+
.DS_Store
2+
node_modules
3+
build
4+
.svelte-kit
5+
/package
6+
.env
7+
.env.*
8+
!.env.example
9+
vite.config.js.timestamp-*
10+
vite.config.ts.timestamp-*
11+
.temp-files

examples/counter-library/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
},
2424
"devDependencies": {
2525
"@sveltejs/adapter-auto": "^2.0.0",
26-
"@sveltejs/kit": "^1.5.0",
26+
"@sveltejs/kit": "^1.12.0",
2727
"@sveltejs/package": "^2.0.0",
2828
"prettier": "^2.8.0",
2929
"prettier-plugin-svelte": "^2.8.1",
3030
"publint": "^0.1.9",
31-
"svelte": "^3.54.0",
31+
"svelte": "^3.57.0",
3232
"svelte-check": "^3.0.1",
3333
"tslib": "^2.4.1",
3434
"typescript": "^4.9.3",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.DS_Store
2+
node_modules
3+
/build
4+
/.svelte-kit
5+
/package
6+
.env
7+
.env.*
8+
!.env.example
9+
10+
# Ignore files for PNPM, NPM and YARN
11+
pnpm-lock.yaml
12+
package-lock.json
13+
yarn.lock
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
root: true,
3+
extends: ['eslint:recommended', 'prettier'],
4+
plugins: ['svelte3'],
5+
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
6+
parserOptions: {
7+
sourceType: 'module',
8+
ecmaVersion: 2020
9+
},
10+
env: {
11+
browser: true,
12+
es2017: true,
13+
node: true
14+
}
15+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
node_modules
3+
/build
4+
/.svelte-kit
5+
/package
6+
.env
7+
.env.*
8+
!.env.example
9+
vite.config.js.timestamp-*
10+
vite.config.ts.timestamp-*
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
engine-strict=true
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.DS_Store
2+
node_modules
3+
/build
4+
/.svelte-kit
5+
/package
6+
.env
7+
.env.*
8+
!.env.example
9+
10+
# Ignore files for PNPM, NPM and YARN
11+
pnpm-lock.yaml
12+
package-lock.json
13+
yarn.lock
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"useTabs": true,
3+
"singleQuote": true,
4+
"trailingComma": "none",
5+
"printWidth": 100,
6+
"plugins": ["prettier-plugin-svelte"],
7+
"pluginSearchDirs": ["."],
8+
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SvelteKit file uploads using Node.js
2+
3+
This example demonstrates how you can upload files with SvelteKit and Node.js in two different ways.
4+
5+
**Note:** This code makes use of the `Readable.fromWeb()` API to convert between web streams and Node.js streams so Node.js >= 17.0.0 is required.
6+
7+
Both forms write files to the local disk into a directory specified by the `FILES_DIR` environment variable. Uploaded files are served through the `src/routes/files/[name]/+server.js` endpoint.
8+
9+
## Form 1: Small files
10+
11+
This first form sends the file as `FormData` to the SvelteKit server.
12+
13+
- It works with and without JavaScript
14+
- It uses FormData and SvelteKit's form actions
15+
- It should only be used for small files such as avatar images because the whole file first needs to be parsed in memory with `event.request.formData()` and there is no upload progress indicator.
16+
17+
## Form 2: Small and large files
18+
19+
The second form for both small and large files uses a custom store and posts the raw file body to the `upload/+server.js` endpoint.
20+
21+
- JavaScript is required for this to work
22+
- A custom store handles the request and calculates the upload progress
23+
- `XMLHTTPRequest` is used to make the requests because `fetch` cannot be used (yet) to calculate the upload progress
24+
- The file object from the file input element is used as the body of the request
25+
- Additional information such as the file's name is passed to the server using custom request headers such as `x-file-name`
26+
- If a file with the same name has already been uploaded before the endpoint closes the connection by calling `event.body.cancel()`
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"extends": "./.svelte-kit/tsconfig.json",
3+
"compilerOptions": {
4+
"allowJs": true,
5+
"checkJs": true,
6+
"esModuleInterop": true,
7+
"forceConsistentCasingInFileNames": true,
8+
"resolveJsonModule": true,
9+
"skipLibCheck": true,
10+
"sourceMap": true,
11+
"strict": true
12+
}
13+
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
14+
//
15+
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16+
// from the referenced tsconfig.json - TypeScript does not merge them in
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "sveltekit-file-uploads-nodejs",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"dev": "vite dev",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
10+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
11+
"lint": "prettier --plugin-search-dir . --check . && eslint .",
12+
"format": "prettier --plugin-search-dir . --write ."
13+
},
14+
"devDependencies": {
15+
"@sveltejs/adapter-node": "^1.2.3",
16+
"@sveltejs/kit": "^1.12.0",
17+
"eslint": "^8.36.0",
18+
"eslint-config-prettier": "^8.7.0",
19+
"eslint-plugin-svelte3": "^4.0.0",
20+
"prettier": "^2.8.4",
21+
"prettier-plugin-svelte": "^2.9.0",
22+
"svelte": "^3.57.0",
23+
"svelte-check": "^3.1.4",
24+
"typescript": "^4.9.5",
25+
"vite": "^4.2.0"
26+
},
27+
"type": "module"
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// See https://kit.svelte.dev/docs/types#app
2+
// for information about these interfaces
3+
declare global {
4+
namespace App {
5+
// interface Error {}
6+
// interface Locals {}
7+
// interface PageData {}
8+
// interface Platform {}
9+
}
10+
}
11+
12+
export {};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
6+
<meta name="viewport" content="width=device-width" />
7+
%sveltekit.head%
8+
</head>
9+
<body data-sveltekit-preload-data="hover">
10+
<div style="display: contents">%sveltekit.body%</div>
11+
</body>
12+
</html>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/* RESET */
2+
3+
*,
4+
*::before,
5+
*::after {
6+
box-sizing: border-box;
7+
padding: 0;
8+
margin: 0;
9+
}
10+
11+
html,
12+
body {
13+
height: 100%;
14+
font-family: sans-serif;
15+
}
16+
17+
html:focus-within {
18+
scroll-behavior: smooth;
19+
}
20+
21+
body {
22+
line-height: 1.5;
23+
}
24+
25+
a:not([class]) {
26+
text-decoration-skip-ink: auto;
27+
}
28+
29+
img,
30+
picture,
31+
video,
32+
canvas {
33+
display: block;
34+
max-width: 100%;
35+
}
36+
37+
input,
38+
button,
39+
textarea,
40+
select,
41+
input[type='file']::file-selector-button {
42+
font: inherit;
43+
}
44+
45+
label,
46+
textarea,
47+
progress,
48+
input:where(
49+
[type='email'],
50+
[type='password'],
51+
[type='search'],
52+
[type='tel'],
53+
[type='text'],
54+
[type='url'],
55+
[type='file']
56+
) {
57+
display: block;
58+
width: 100%;
59+
}
60+
61+
textarea {
62+
box-sizing: content-box;
63+
min-height: 5em;
64+
resize: block;
65+
resize: vertical;
66+
}
67+
68+
/* THEME */
69+
70+
h1 {
71+
margin-block-end: 2rem;
72+
}
73+
74+
h2,
75+
h3 {
76+
margin-block-end: 1rem;
77+
}
78+
79+
label {
80+
font-weight: 700;
81+
margin-block-end: 0.25rem;
82+
}
83+
84+
button,
85+
input[type='file']::file-selector-button {
86+
padding-inline: 0.5rem;
87+
padding-block: 0.2rem;
88+
cursor: pointer;
89+
}
90+
91+
button:hover {
92+
filter: brightness(1.1);
93+
}
94+
95+
button.--loading {
96+
cursor: wait;
97+
opacity: 0.6;
98+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { writable } from 'svelte/store';
2+
3+
export function create_upload() {
4+
const { subscribe, update } = writable({ status: 'idle', progress: 0 });
5+
6+
/** @type {XMLHttpRequest} */
7+
let xhr;
8+
9+
return {
10+
subscribe,
11+
12+
/** @param {{file: File, url: string, headers: Record<string, string>}} input */
13+
start({ file, url, headers = {} }) {
14+
return new Promise((resolve, reject) => {
15+
xhr = new XMLHttpRequest();
16+
17+
xhr.upload.addEventListener('progress', (event) => {
18+
/** @type {number} */
19+
let progress = 0;
20+
21+
if (event.lengthComputable) {
22+
progress = Math.round((event.loaded / event.total) * 100);
23+
}
24+
25+
update((state) => ({ ...state, status: 'uploading', progress }));
26+
});
27+
28+
xhr.addEventListener('loadend', () => {
29+
const status = xhr.status > 0 && xhr.status < 400 ? 'completed' : 'error';
30+
31+
update((state) => ({ ...state, status }));
32+
33+
resolve(xhr);
34+
});
35+
36+
xhr.upload.addEventListener('error', () => {
37+
update((state) => ({ ...state, progress: 0, status: 'error' }));
38+
});
39+
40+
xhr.open('POST', url);
41+
42+
for (const [name, value] of Object.entries(headers)) {
43+
xhr.setRequestHeader(name, value);
44+
}
45+
46+
xhr.send(file);
47+
});
48+
}
49+
};
50+
}
51+
52+
export default create_upload;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
import '$lib/css/styles.css';
3+
</script>
4+
5+
<main>
6+
<slot />
7+
</main>
8+
9+
<style>
10+
main {
11+
padding: 1rem;
12+
}
13+
14+
:global(main > *) {
15+
max-width: 60rem;
16+
margin-inline: auto;
17+
}
18+
</style>

0 commit comments

Comments
 (0)