Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,23 @@ CREATE TABLE IF NOT EXISTS jobs (
num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);
PRAGMA user_version = 1;`);
CREATE TABLE IF NOT EXISTS storage_metadata (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
job_id INTEGER NOT NULL,
file_name TEXT NOT NULL,
storage_key TEXT NOT NULL,
FOREIGN KEY (job_id) REFERENCES jobs(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
PRAGMA user_version = 2;`);
}

const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1.");
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version!;
if (dbVersion < 2) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
db.exec("ALTER TABLE file_names ADD COLUMN storage_key TEXT;");
db.exec("PRAGMA user_version = 2;");
console.log("Updated database to version 2.");
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

// enable WAL mode
Expand Down
51 changes: 17 additions & 34 deletions src/pages/download.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,46 @@
import path from "node:path";
import { Elysia } from "elysia";
import sanitize from "sanitize-filename";
import * as tar from "tar";
import { outputDir } from "..";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user";
import { getStorage } from "../storage/index";

export const download = new Elysia()
.use(userService)
.get(
"/download/:userId/:jobId/:fileName",
async ({ params, redirect, user }) => {
const userId = user.id;
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);

if (!job) {
return redirect(`${WEBROOT}/results`, 302);
}
// parse from URL encoded string
const jobId = decodeURIComponent(params.jobId);

const fileName = sanitize(decodeURIComponent(params.fileName));

const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
return Bun.file(filePath);
},
{
auth: true,
},
)
.get(
"/archive/:jobId",
async ({ params, redirect, user }) => {
const userId = user.id;
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
const fileRow = db
.query(`
SELECT storage_key FROM file_names
WHERE job_id = ? AND file_name = ?
`,
)
.get(params.jobId, fileName) as { storage_key: string } | undefined;

if (!job) {
if (!fileRow) {
return redirect(`${WEBROOT}/results`, 302);
}

const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}`;
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
const storage = getStorage();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const fileBuffer = await storage.get(fileRow.storage_key);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

await tar.create(
{
file: outputTar,
cwd: outputPath,
filter: (path) => {
return !path.match(".*\\.tar");
},
return new Response(fileBuffer, {
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
},
["."],
);
return Bun.file(outputTar);
});
},
{
auth: true,
Expand Down
25 changes: 19 additions & 6 deletions src/pages/upload.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Elysia, t } from "elysia";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index";
import { userService } from "./user";
import sanitize from "sanitize-filename";
import { getStorage } from "../storage";
import crypto from "node:crypto";

export const upload = new Elysia().use(userService).post(
"/upload",
Expand All @@ -12,6 +13,8 @@ export const upload = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302);
}

const jobIdValue = jobId.value;

const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
Expand All @@ -20,17 +23,27 @@ export const upload = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302);
}

const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
const storage = getStorage();

const saveFile = async (file: File) => {
const sanitizedFileName = sanitize(file.name);
const storageKey = `${user.id}/${jobId.value}/${crypto.randomUUID()}`;
const buffer = Buffer.from(await file.arrayBuffer());
await storage.save(storageKey, buffer);

db.query(`
INSERT INTO file_names (job_id, file_name, storage_key)
VALUES (?, ?, ?)
`).run(jobIdValue, sanitizedFileName, storageKey);
};

if (body?.file) {
if (Array.isArray(body.file)) {
for (const file of body.file) {
const santizedFileName = sanitize(file.name);
await Bun.write(`${userUploadsDir}${santizedFileName}`, file);
await saveFile(file);
}
} else {
const santizedFileName = sanitize(body.file["name"]);
await Bun.write(`${userUploadsDir}${santizedFileName}`, body.file);
await saveFile(body.file);;
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/storage/LocalStorageAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { IStorageAdapter } from "./index";
import { promises as fs } from "fs";
import path from "path";

export class LocalStorageAdapter implements IStorageAdapter {
baseDir: string;

constructor(baseDir: string) {
this.baseDir = baseDir;
}

async save(key: string, data: Buffer): Promise<string> {
const fullPath = path.join(this.baseDir, key);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, data);
return key;
}

async get(key: string): Promise<Buffer> {
const fullPath = path.join(this.baseDir, key);
return fs.readFile(fullPath);
}

async delete(key: string): Promise<void> {
const fullPath = path.join(this.baseDir, key);
await fs.unlink(fullPath);
}
}
36 changes: 36 additions & 0 deletions src/storage/S3StorageAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { s3, S3File } from "bun";
import { IStorageAdapter } from "./index";

export class S3StorageAdapter implements IStorageAdapter {
private bucket: string;

constructor(bucket: string) {
this.bucket = bucket;
}

async save(key: string, data: Buffer): Promise<string> {
const file: S3File = s3.file(key, {
bucket: this.bucket,
acl: "private",
});

await file.write(data);
return key;
}

async get(key: string): Promise<Buffer> {
const file: S3File = s3.file(key, {
bucket: this.bucket,
});

return Buffer.from(await file.bytes());
}

async delete(key: string): Promise<void> {
const file: S3File = s3.file(key, {
bucket: this.bucket,
})

await file.delete();
}
}
20 changes: 20 additions & 0 deletions src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { LocalStorageAdapter } from "./LocalStorageAdapter";
import { S3StorageAdapter } from "./S3StorageAdapter";

export interface IStorageAdapter {
save(key: string, data: Buffer): Promise<string>;
get(key: string): Promise<Buffer>;
delete(key: string): Promise<void>;
}

export function getStorage(): IStorageAdapter {
if (process.env.STORAGE_BACKEND === "s3") {
if (!process.env.S3_BUCKET) {
throw new Error("S3_BUCKET must be set when STORAGE_BACKEND=s3");
}

return new S3StorageAdapter(process.env.S3_BUCKET);
}

return new LocalStorageAdapter("./data");
}