feat: page uploading support

This commit is contained in:
Guz
2024-09-23 20:52:40 -03:00
parent df9f42decd
commit b2e148e252
17 changed files with 457 additions and 23 deletions

5
.example.env Normal file
View File

@@ -0,0 +1,5 @@
AWS_ACCESS_KEY_ID=**************************
AWS_SECRET_ACCESS_KEY=****************************************************************
AWS_DEFAULT_REGION=******
AWS_ENDPOINT_URL=localhost:3000
S3_DEFAULT_BUCKET=comicverse-test-bucket

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
data.db

BIN
bun.lockb

Binary file not shown.

View File

@@ -22,10 +22,11 @@
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
buildInputs = with pkgs; [
awscli2
bun
eslint
nodejs_22
nodePackages_latest.prettier
bun
];
};
});

View File

@@ -1,6 +1,28 @@
{
"name": "comicverse",
"version": "0.0.1",
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^9.6.0",
"@types/node": "^22.6.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"minio": "^8.0.1",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3",
"xml-js": "^1.6.11"
},
"private": true,
"scripts": {
"dev": "vite dev",
@@ -11,22 +33,5 @@
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^9.6.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3"
},
"type": "module"
}

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />

View File

@@ -1 +1,3 @@
// place files you want to import through the `$lib` alias in this folder.
export { default as s3 } from './s3';
export { default as db } from './sqlite';
export * from './sqlite'

19
src/lib/s3.ts Normal file
View File

@@ -0,0 +1,19 @@
import {
AWS_ENDPOINT_URL,
AWS_ACCESS_KEY_ID,
AWS_DEFAULT_REGION,
AWS_SECRET_ACCESS_KEY
} from '$env/static/private';
import * as Minio from 'minio';
const client = new Minio.Client({
endPoint: AWS_ENDPOINT_URL.split(':')[0],
port: Number(AWS_ENDPOINT_URL.split(':')[1]),
useSSL: false,
region: AWS_DEFAULT_REGION,
accessKey: AWS_ACCESS_KEY_ID,
secretKey: AWS_SECRET_ACCESS_KEY
});
export default client;

28
src/lib/sqlite.ts Normal file
View File

@@ -0,0 +1,28 @@
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
const db = await open({
filename: 'data.db',
driver: sqlite3.cached.Database
});
await db.exec(`
CREATE TABLE IF NOT EXISTS projects (
ID text NOT NULL,
Name text NOT NULL,
PRIMARY KEY(ID)
)
`);
type Project = {
id: string;
title: string;
pages: {
title: string;
src: string;
background: string;
}[];
};
export type { Project };
export default db;

21
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,21 @@
<svelte:head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css"
/>
<meta name="color-scheme" content="dark" />
</svelte:head>
<main>
<slot></slot>
</main>
<style>
main {
width: 100vw;
height: 100vh;
}
</style>

View File

@@ -0,0 +1,40 @@
import { fail, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db, s3, type Project } from '$lib';
import { AWS_S3_DEFAULT_BUCKET } from '$env/static/private';
export const load = (async ({}) => {
const res = await db.all<Project[]>('SELECT ID, Name FROM projects');
return { projects: res };
}) as PageServerLoad;
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const name = data.get('project-name');
if (!name) return fail(400, { name, missing: true });
const uuid = crypto.randomUUID().split('-')[0];
const res = await db.run('INSERT OR IGNORE INTO projects (ID, Name) VALUES (:id, :name)', {
':id': uuid,
':name': name
});
const project: Project = {
id: uuid,
title: name.toString(),
pages: []
};
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${uuid}/project.json`, JSON.stringify(project));
if (res.changes == undefined) {
return fail(500, { reason: 'Failed to insert project into database' });
}
return { success: true };
}
};

View File

@@ -1,2 +1,46 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script lang="ts">
import type { Project } from '$lib';
import type { PageData } from './$types';
export let data: PageData;
if ((data.projects.length + 1) % 3 !== 0) {
data.projects.push({ Name: '', ID: '' });
data.projects.push({ Name: '', ID: '' });
}
</script>
<section>
{#each data.projects as p}
<article>
<h1><a href={`/projects/${p.ID}`}>{p.Name}</a></h1>
<p class="id">{p.ID}</p>
</article>
{/each}
<article>
<form method="POST">
<fieldset role="group">
<input type="text" name="project-name" placeholder="Project Name" required />
<input type="submit" value="Create" />
</fieldset>
</form>
</article>
</section>
<style>
section {
display: grid;
@media (min-width: 768px) {
grid-template-columns: repeat(3, minmax(0%, 1fr));
}
grid-column-gap: var(--pico-grid-column-gap);
grid-row-gap: var(--pico-grid-row-gap);
padding: 1rem var(--pico-grid-row-gap);
}
.id {
font-size: 0.7rem;
opacity: 0.3;
}
</style>

View File

@@ -0,0 +1,41 @@
import { type RequestHandler } from '@sveltejs/kit';
import stream from 'node:stream/promises';
import { db, s3, type Project } from '$lib';
import { AWS_S3_DEFAULT_BUCKET } from '$env/static/private';
import { extname } from 'node:path';
export const GET = (async ({ params }) => {
const file = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.project}/${params.file}`);
file.on('error', (err: any) => {
console.log(err);
});
let chunks: Buffer[] = [];
let buf;
file.on('data', (chunk) => {
chunks.push(Buffer.from(chunk));
});
file.on('end', () => {
buf = Buffer.concat(chunks);
});
await stream.finished(file)
let res = new Response(buf);
res.headers.set(
'Content-Type',
(() => {
switch (extname(params.file!)) {
case '.png':
return 'image/png';
case '.json':
return 'application/json';
}
return 'text/plain';
})()
);
res.headers.set('Cache-Control', 'max-age=604800')
return res;
}) as RequestHandler;

View File

@@ -0,0 +1,76 @@
import { error, fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import stream from 'node:stream/promises';
import { db, s3, type Project } from '$lib';
import { AWS_S3_DEFAULT_BUCKET } from '$env/static/private';
import { extname } from 'node:path';
export const load = (async ({ params }) => {
const res = await db.get<{ id: string; name: string }>(
'SELECT ID, Name FROM projects WHERE ID = ?',
params.id
);
if (res === undefined) {
return fail(404, { reason: 'Failed to find project into database' });
}
const project = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
project.on('error', (err: any) => {
console.log(err);
});
let p: string = '';
project.on('data', (chunk: any) => {
p += chunk;
});
await stream.finished(project);
let proj = JSON.parse(p) as Project;
return { project: proj };
}) as PageServerLoad;
export const actions = {
delete: async ({ params }) => {
const res = await db.run('DELETE FROM projects WHERE ID = ?', params.id);
await s3.removeObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
if (res === undefined) {
return fail(500, { reason: 'Failed to delete project' });
}
redirect(303, '/');
},
addpage: async ({ request, params }) => {
const form = await request.formData();
const file = form?.get('file') as File;
const title = form?.get('title') as string;
const color = form?.get('color') as string;
console.log(file);
const project = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
project.on('error', (err: any) => {
console.log(err);
});
let p: string = '';
project.on('data', (chunk: any) => {
p += chunk;
});
await stream.finished(project);
let proj = JSON.parse(p) as Project;
const filename = `${crypto.randomUUID().split('-')[0]}${extname(file?.name)}`;
proj.pages.push({
title: title,
src: filename,
background: color
});
const buf = Buffer.from(await file.arrayBuffer());
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`, JSON.stringify(proj));
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/${filename}`, buf);
}
} as Actions;

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const pages = data.project.pages;
let modal = false;
let reader, scroll;
let color = pages[0].background;
</script>
<pre style="position: fixed; bottom: 0; font-size: 0.6rem;">
<code
>{JSON.stringify(
{
color: color,
scroll: scroll
},
null,
2
)}
</code>
</pre>
<dialog open={modal}>
<article>
<header>
<button
aria-label="Close"
rel="prev"
on:click={() => {
modal = false;
}}
></button>
<p>
<strong>Add new page</strong>
</p>
</header>
<form method="POST" action="?/addpage" enctype="multipart/form-data">
<input type="text" required placeholder="Page title" name="title" />
<input type="color" required placeholder="Background color" name="color" />
<input type="file" required name="file" />
<input type="submit" value="Add page" />
</form>
</article>
</dialog>
<section class="project">
<aside>
<section>
<h1>{data.project.title}</h1>
<p class="id">{data.project.id}</p>
<button
class="add"
on:click={() => {
modal = true;
}}>Add page</button
>
<form method="POST">
<input type="submit" formaction="?/delete" value="Delete" class="pico-background-red-500" />
</form>
</section>
</aside>
<article
class="reader"
style={`--bg-color: ${color}`}
bind:this={reader}
on:scroll={() => (scroll = reader.scrollTop)}
>
<div class="pages">
{#each pages as page}
<div class="page" style={`background-color:${page.background}`}>
<img width="1080" height="1920" src={`/files/${data.project.id}/${page.src}`} />
<form method="POST" action="?/delete-file" class="delete-file">
<fieldset role="group">
<input type="text" disabled value={`${page.src}`} name="file" />
<input type="submit" value="Delete page" class="pico-background-red-500" />
</fieldset>
</form>
</div>
{/each}
</div>
</article>
</section>
<style>
h1 {
font-size: 1.5rem;
}
.id {
font-size: 0.5rem;
opacity: 0.3;
}
.reader {
display: flex;
width: 80vw;
justify-content: center;
padding-top: 5rem;
padding-bottom: 5rem;
margin-bottom: 0;
background-color: var(--bg-color);
height: 100vh;
overflow-y: scroll;
}
.page {
width: calc(1080px / 2.5);
min-height: calc(1920px / 2.5);
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0;
& form {
margin: 1rem;
margin-bottom: 0;
}
}
.pages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.project {
display: flex;
margin-bottom: 0;
}
.add {
width: 100%;
margin-bottom: 0.5rem;
}
aside {
padding: 1rem;
width: 20vw;
}
</style>

1
temp.json Normal file
View File

@@ -0,0 +1 @@
{"id":"88ba21ea","title":"test project","pages":[]}

View File

@@ -2,5 +2,9 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
server: {
port: 3000,
host: '192.168.1.7'
}
});