Skip to content

Commit b99a265

Browse files
Add bucket to file upload example (#190)
1 parent ef45a4e commit b99a265

File tree

4 files changed

+75
-34
lines changed

4 files changed

+75
-34
lines changed

ts/file-upload/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ts/file-upload/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"@types/busboy": "^1.5.4"
1212
},
1313
"dependencies": {
14-
"encore.dev": "^1.40.1",
14+
"encore.dev": "^1.45.1",
1515
"busboy": "^1.6.0"
1616
},
1717
"optionalDependencies": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
CREATE TABLE files (
22
name TEXT PRIMARY KEY,
3+
mime_type TEXT NOT NULL,
34
data BYTEA NOT NULL
45
);

ts/file-upload/upload/upload.ts

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ import { api } from "encore.dev/api";
22
import log from "encore.dev/log";
33
import busboy from "busboy";
44
import { SQLDatabase } from "encore.dev/storage/sqldb";
5-
import { APICallMeta, appMeta, currentRequest } from "encore.dev";
5+
import { APICallMeta, appMeta, currentRequest } from "encore.dev"; // Define a bucket named 'profile-files' for storing files.
6+
import { Bucket } from "encore.dev/storage/objects";
7+
8+
// Define a bucket named 'profile-files' for storing files.
9+
// Making it possible to get public URLs to files in the bucket
10+
// by setting 'public' to true
11+
export const filesBucket = new Bucket("profile-files", {
12+
versioned: false,
13+
public: true,
14+
});
615

716
// Define a database named 'files', using the database migrations
817
// in the "./migrations" folder. Encore automatically provisions,
@@ -11,7 +20,7 @@ export const DB = new SQLDatabase("files", {
1120
migrations: "./migrations",
1221
});
1322

14-
type FileEntry = { data: any[]; filename: string };
23+
type FileEntry = { data: any[]; filename: string; mimeType: string };
1524

1625
/**
1726
* Raw endpoint for storing a single file to the database.
@@ -24,9 +33,10 @@ export const save = api.raw(
2433
headers: req.headers,
2534
limits: { files: 1 },
2635
});
27-
const entry: FileEntry = { filename: "", data: [] };
36+
const entry: FileEntry = { filename: "", data: [], mimeType: "" };
2837

2938
bb.on("file", (_, file, info) => {
39+
entry.mimeType = info.mimeType;
3040
entry.filename = info.filename;
3141
file
3242
.on("data", (data) => {
@@ -43,11 +53,19 @@ export const save = api.raw(
4353
bb.on("close", async () => {
4454
try {
4555
const buf = Buffer.concat(entry.data);
56+
57+
// Save file to bucket
58+
await filesBucket.upload(entry.filename, buf, {
59+
contentType: entry.mimeType,
60+
});
61+
62+
// Save file to DB
4663
await DB.exec`
47-
INSERT INTO files (name, data)
48-
VALUES (${entry.filename}, ${buf})
64+
INSERT INTO files (name, data, mime_type)
65+
VALUES (${entry.filename}, ${buf}, ${entry.mimeType})
4966
ON CONFLICT (name) DO UPDATE
50-
SET data = ${buf}
67+
SET data = ${buf},
68+
mime_type = ${entry.mimeType}
5169
`;
5270
log.info(`File ${entry.filename} saved`);
5371

@@ -80,7 +98,7 @@ export const saveMultiple = api.raw(
8098
const entries: FileEntry[] = [];
8199

82100
bb.on("file", (_, file, info) => {
83-
const entry: FileEntry = { filename: info.filename, data: [] };
101+
const entry: FileEntry = { filename: "", data: [], mimeType: "" };
84102

85103
file
86104
.on("data", (data) => {
@@ -98,11 +116,19 @@ export const saveMultiple = api.raw(
98116
try {
99117
for (const entry of entries) {
100118
const buf = Buffer.concat(entry.data);
119+
120+
// Save file to Bucket
121+
await filesBucket.upload(entry.filename, buf, {
122+
contentType: entry.mimeType,
123+
});
124+
125+
// Save file to DB
101126
await DB.exec`
102-
INSERT INTO files (name, data)
103-
VALUES (${entry.filename}, ${buf})
127+
INSERT INTO files (name, data, mime_type)
128+
VALUES (${entry.filename}, ${buf}, ${entry.mimeType})
104129
ON CONFLICT (name) DO UPDATE
105-
SET data = ${buf}
130+
SET data = ${buf},
131+
mime_type = ${entry.mimeType}
106132
`;
107133
log.info(`File ${entry.filename} saved`);
108134
}
@@ -125,16 +151,6 @@ export const saveMultiple = api.raw(
125151
},
126152
);
127153

128-
// Helper function for saving a file to the database
129-
const saveToDb = async (name: string, data: Buffer) => {
130-
return await DB.exec`
131-
INSERT INTO files (name, data)
132-
VALUES (${name}, ${data})
133-
ON CONFLICT (name) DO UPDATE
134-
SET data = ${data}
135-
`;
136-
};
137-
138154
// Raw endpoint for serving a file from the database
139155
export const get = api.raw(
140156
{ expose: true, method: "GET", path: "/files/:name" },
@@ -151,6 +167,7 @@ export const get = api.raw(
151167
return;
152168
}
153169

170+
resp.writeHead(200, { "Content-Type": row.mime_type });
154171
const chunk = Buffer.from(row.data);
155172
resp.writeHead(200, { Connection: "close" });
156173
resp.end(chunk);
@@ -166,8 +183,8 @@ interface ListResponse {
166183
}
167184

168185
// API endpoint for listing all files in the database
169-
export const list = api(
170-
{ expose: true, method: "GET", path: "/files" },
186+
export const listDBFiles = api(
187+
{ expose: true, method: "GET", path: "/db-files" },
171188
async (): Promise<ListResponse> => {
172189
const rows = await DB.query`
173190
SELECT name
@@ -188,6 +205,23 @@ export const list = api(
188205
},
189206
);
190207

208+
// API endpoint for listing all files in the bucket
209+
export const listBucketFiles = api(
210+
{ expose: true, method: "GET", path: "/bucket-files" },
211+
async (): Promise<ListResponse> => {
212+
const resp: ListResponse = { files: [] };
213+
214+
for await (const entry of filesBucket.list({})) {
215+
resp.files.push({
216+
url: filesBucket.publicUrl(entry.name),
217+
name: entry.name,
218+
});
219+
}
220+
221+
return resp;
222+
},
223+
);
224+
191225
// Serving some static HTML for demo purposes
192226
export const frontend = api.raw(
193227
{ expose: true, path: "/!path", method: "GET" },
@@ -209,12 +243,17 @@ export const frontend = api.raw(
209243
<input type="submit">
210244
</form>
211245
<br/>
212-
<h2>Files:</h2>
246+
<h2>Files in DB:</h2>
247+
<div id="bd-files"></div>
248+
249+
<h2>Files in Bucket:</h2>
250+
<div id="bucket-files"></div>
213251
214252
<script>
215-
async function getData() {
253+
async function getData(elementId, url) {
254+
const el = document.getElementById(elementId);
216255
try {
217-
const response = await fetch("/files");
256+
const response = await fetch(url);
218257
const json = await response.json();
219258
const list = document.createElement("ul");
220259
json.files.forEach((file) => {
@@ -225,12 +264,13 @@ export const frontend = api.raw(
225264
item.appendChild(link);
226265
list.appendChild(item);
227266
});
228-
document.body.appendChild(list);
267+
el.appendChild(list);
229268
} catch (error) {
230269
console.error(error.message);
231270
}
232271
}
233-
getData();
272+
getData("bd-files", "/db-files");
273+
getData("bucket-files", "/bucket-files");
234274
</script>
235275
</body>
236276
</html>

0 commit comments

Comments
 (0)