From c72ac5e67f0a2967d43ecd7a5bcb831d2464beff Mon Sep 17 00:00:00 2001 From: Andreas Schaafsma Date: Wed, 12 Jun 2024 18:08:08 +0200 Subject: [PATCH] Working state for attachments and buckets --- src/app/api/attachment/route.ts | 93 +++++++++++++++++++++------------ src/app/api/setupDB/route.ts | 47 +++++++++++++---- src/model/Attachment.ts | 20 ++++--- src/model/Bucket.ts | 32 ++++++++++++ src/model/Post.ts | 20 +++++-- 5 files changed, 156 insertions(+), 56 deletions(-) create mode 100644 src/model/Bucket.ts diff --git a/src/app/api/attachment/route.ts b/src/app/api/attachment/route.ts index ba025f5..35f9882 100644 --- a/src/app/api/attachment/route.ts +++ b/src/app/api/attachment/route.ts @@ -4,19 +4,65 @@ import { APIError, attemptAPIAction } from "@/util/api/error"; import { Auth, Post, PostTag, Tag, User } from "@/model/Models"; import { cookies } from "next/headers"; import { Attachment } from "@/model/Attachment"; -import { randomUUID } from "crypto"; +import { UUID, randomUUID } from "crypto"; import { mkdir, mkdirSync, writeFile } from "fs"; +import { Bucket } from "@/model/Bucket"; +import { where } from "@sequelize/core"; +import { PostBucket } from "@/model/Post"; + + +async function writeFilesToBucket(uuid: UUID, files:any[]) { + const fileArray:{name:string, content:Promise>|ReadableStreamReadResult}[] = await Promise.all( files.map((file:File) => { + return {'name':( ()=>file.name)(), 'content':file.stream().getReader().read()}; + })); + + let finalFileArray:{name:string,content:ReadableStreamReadResult}[] = await (async () => { + for(let file in fileArray){ + fileArray[file].content = await fileArray[file].content + } + return [...fileArray as {name:string,content:ReadableStreamReadResult}[]] + })() ; + + mkdirSync(`./bucket/${uuid}/`) + for(let file in finalFileArray){ + + writeFile(`./bucket/${uuid}/${finalFileArray[file].name}`,Buffer.from(finalFileArray[file].content.value as Uint8Array),(e)=>{console.log(e)}) + const attachment = await Attachment.create({bucket_id:uuid, filename:finalFileArray[file].name}, {include: Attachment.associations.bucket}); + console.log(attachment); + } +} + +async function addToPost(postid:number):Promise +{ + const post = await Post.findOne({where: {id:postid}, include: {association: Post.associations.postBuckets}}); + if (!post) throw new APIError({ status: 500, responseText: "invalid postid" }); + const bucket = await Bucket.create({id:randomUUID()}); + const bucketPost = await PostBucket.create({bucketId: bucket.id, postId: postid}) + + console.log(bucketPost); + return bucket.id +} +async function addToBucket(bucketid:number):Promise { + const bucket = await Bucket.findOne({where: {id: bucketid}}); + if (!bucket) throw new APIError({ status: 500, responseText: "invalid bucketid" }); + return bucket.id +} + + async function tryCreateAttachment(request: Request) { // Make sure the DB is ready await Attachment.sync(); + await Bucket.sync(); await Post.sync(); - + // Prepare data const formData = await request.formData(); + const requestData:string | Object | undefined = formData.get('data')?.valueOf(); + const files:FormDataEntryValue[] = formData.getAll('files') const authCkie = await cookies().get("auth"); // Sanity check auth cookie @@ -39,46 +85,27 @@ async function tryCreateAttachment(request: Request) { where: { token: authObject.token } }); - // Sanity check the auth and associated user + + // Sanity check the auth and associated user for authorization if (!auth || !auth.user) throw new APIError({ status: 401, responseText: "Authentication Error" }); - - // Handle incomplete data or other problems - if (!formData) throw new APIError({ status: 500, responseText: "Empty request body" }); - const files:any[] = formData.getAll('files') - - if (!files) throw new APIError({ status: 500, responseText: "Missing file" }); if (!auth.user.id) throw new APIError({ status: 500, responseText: "Missing user id" }); if (!auth.user.perms || !auth.user.perms.isAdmin) throw new APIError({ status: 401, responseText: `Unauthorized ${JSON.stringify(auth.user)}` }); - - // peepee - const uuid = randomUUID() - const fileArray:{name:string, content:Promise>|ReadableStreamReadResult}[] = await Promise.all( files.map((file:File) => { - return {'name':( ()=>file.name)(), 'content':file.stream().getReader().read()}; - })); - + // Handle incomplete data or other problems + if (!files) throw new APIError({ status: 500, responseText: "Missing file" }); + if (!formData) throw new APIError({ status: 500, responseText: "Empty request body" }); + if (!requestData) throw new APIError({ status: 500, responseText: "Missing request data" }); + if (!(typeof requestData == "string")) throw new APIError({ status: 500, responseText: "Malformed request data" }); - let finalFileArray:{name:string,content:ReadableStreamReadResult}[] = await (async () => { - for(let file in fileArray){ - fileArray[file].content = await fileArray[file].content - } - return [...fileArray as {name:string,content:ReadableStreamReadResult}[]] - })() ; - - mkdirSync(`./bucket/${uuid}/`) - for(let file in finalFileArray){ - - writeFile(`./bucket/${uuid}/${finalFileArray[file].name}`,Buffer.from(finalFileArray[file].content.value as Uint8Array),(e)=>{console.log(e)}) - } - // const kanker = files.map(parseFiles) - + const data = JSON.parse(requestData); + + let uuid:UUID = (data.postid && !data.bucketid)? await addToPost(data.postid) : await addToBucket(data.bucketid) + writeFilesToBucket(uuid, files); - // console.log(await kanker[0]); - return new Response(JSON.stringify({ - 'files': fileArray, + 'files': 'ya yeet', 'uuid': uuid, }), { status: 200 }); diff --git a/src/app/api/setupDB/route.ts b/src/app/api/setupDB/route.ts index 522f022..452686c 100644 --- a/src/app/api/setupDB/route.ts +++ b/src/app/api/setupDB/route.ts @@ -6,11 +6,42 @@ import { APIError} from "@/util/api/error" import { UserAuth, parseBasicAuth, getAssociatedUser } from "@/util/api/user" import { Auth, Post, Tag, User } from "@/model/Models"; import { Project } from "@/model/Project"; +import { Attachment } from "@/model/Attachment"; +import { Bucket } from "@/model/Bucket"; import { DBState } from "@/model/DBState"; import Sequelize, { DataTypes } from "@sequelize/core"; -import { SqliteColumnsDescription, SqliteDialect } from "@sequelize/sqlite3"; +import { SqliteColumnsDescription, SqliteDialect, SqliteQueryInterface } from "@sequelize/sqlite3"; +import { hashPassword } from "@/util/Auth"; +async function seedDatabase(queryInterface:SqliteQueryInterface){ + const password = await hashPassword('changeme'); + const project = await Project.findOne({where: { + readableIdentifier: 'blog' + }}).then(e=> e ? e : Project.create({name:'General Blog',readableIdentifier:'blog'})); + const user = await User.findOne({where: { + username: 'admin' + }}).then(e=> e ? e : User.create({username: 'admin', password: password, perms: {isAdmin: true}}, {include: User.associations.perms})); + await Post.create({ + title: 'Test Post', + content: ` + # Hello + this is some **test** markdown + `, + project_id: project.id, + user_id: user.id + }) + await Post.create({ + title: 'Test Post 2', + content: ` + # Hello + this is amother post with some **test** markdown + `, + project_id: project.id, + user_id: user.id + }) +} + async function trySetup(request:Request){ const sequelize = await new Sequelize({ @@ -21,27 +52,23 @@ async function trySetup(request:Request){ // await User.sync(); await Auth.sync(); await User.sync(); + await Attachment.sync(); + await Bucket.sync(); await Project.sync() await Tag.sync(); await Post.sync(); await DBState.sync(); - const version = await (await DBState.findAll()).sort((a,b)=> ((a.version > b.version) ? 1 : -1)).map(a=>a.version)[0]; + const version = (await DBState.findAll()).sort((a,b)=> ((a.version > b.version) ? 1 : -1)).map(a=>a.version)[0]; - await Project.findOne({where: { - readableIdentifier: 'blog' - }}).then(e=> e ? e : Project.create({name:'General Blog',readableIdentifier:'blog'})); - await User.findOne({where: { - username: 'admin' - }}).then(e=> e ? e : User.create({username: 'admin', password: 'changeme', perms: {isAdmin: true}})); switch(version){ case 1: break; default: - const postsRows:SqliteColumnsDescription = await queryInterface.describeTable('Posts').then(t=>t); - + seedDatabase(queryInterface); + const postsRows:SqliteColumnsDescription = await queryInterface.describeTable(Post.table.tableName).then(t=>t); if (!postsRows['project_id']) queryInterface.addColumn('Posts','project_id',{type: DataTypes.INTEGER, acceptsNull:()=>false,defaultValue:1}) break; } diff --git a/src/model/Attachment.ts b/src/model/Attachment.ts index 79627a8..2cddf00 100644 --- a/src/model/Attachment.ts +++ b/src/model/Attachment.ts @@ -1,26 +1,30 @@ -import { Association, BelongsToGetAssociationMixin, BelongsToManyGetAssociationsMixin, DataTypes, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute, Sequelize } from "@sequelize/core"; +import { Association, BelongsToGetAssociationMixin, BelongsToManyGetAssociationsMixin, CreationOptional, DataTypes, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute, Sequelize } from "@sequelize/core"; import { Post } from "./Post"; import { SqliteDialect } from '@sequelize/sqlite3'; import { Attribute, AutoIncrement, BelongsTo, BelongsToMany, NotNull, PrimaryKey, Unique } from "@sequelize/core/decorators-legacy"; +import { Bucket } from "./Bucket"; +import { UUID } from "crypto"; export class Attachment extends Model, InferCreationAttributes> { @PrimaryKey @AutoIncrement @Attribute(DataTypes.INTEGER) @Unique - declare id: number; + declare id: CreationOptional; @Attribute(DataTypes.STRING) - declare path: string + declare filename: string // Associations - @Attribute(DataTypes.INTEGER) + @Attribute(DataTypes.UUIDV4) @NotNull - declare postid: number; - @BelongsTo(()=>Post,{foreignKey: 'postid', inverse: {type: "hasMany", as: 'attachments'}}) - declare post?:NonAttribute; + declare bucket_id: ForeignKey; + + @BelongsTo(()=>Bucket,{foreignKey: 'bucket_id', inverse: {type: "hasMany", as: 'attachments'}}) + declare bucket?:NonAttribute; + declare static associations: { - posts: Association; + bucket: Association; }; } diff --git a/src/model/Bucket.ts b/src/model/Bucket.ts new file mode 100644 index 0000000..e4d7f11 --- /dev/null +++ b/src/model/Bucket.ts @@ -0,0 +1,32 @@ +import { Association, BelongsToGetAssociationMixin, BelongsToManyGetAssociationsMixin, DataTypes, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute, Sequelize, sql } from "@sequelize/core"; +import { Post } from "./Post"; + +import { SqliteDialect } from '@sequelize/sqlite3'; +import { Attribute, AutoIncrement, BelongsTo, BelongsToMany, Default, NotNull, PrimaryKey, Unique } from "@sequelize/core/decorators-legacy"; +import { Attachment } from "./Attachment"; +import { UUID } from "crypto"; + +export class Bucket extends Model, InferCreationAttributes> { + @PrimaryKey + @Unique + @Attribute(DataTypes.UUIDV4) + @Default(sql.uuidV4) + declare id: UUID + + /** Defined by {@link Post.buckets} */ + declare bucketPosts?:NonAttribute[]; + + /** Defined by {@link Attachment.bucket} */ + declare attachments?:NonAttribute[]; + + declare static associations: { + bucketPosts: Association; + attachments: Association; + }; +} + +const sequelize = new Sequelize({ + dialect: SqliteDialect, + storage: 'db.sqlite', + models: [Bucket] +}) diff --git a/src/model/Post.ts b/src/model/Post.ts index 7caed96..3904243 100644 --- a/src/model/Post.ts +++ b/src/model/Post.ts @@ -1,4 +1,4 @@ -import { Association, BelongsToGetAssociationMixin, CreationOptional, DataTypes, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute, Sequelize } from "@sequelize/core";import { User } from "./User"; +import { Association, BelongsToGetAssociationMixin, BelongsToManyAssociation, BelongsToManyCreateAssociationMixin, CreationOptional, DataTypes, ForeignKey, InferAttributes, InferCreationAttributes, Model, NonAttribute, Sequelize } from "@sequelize/core";import { User } from "./User"; import { SqliteDialect } from '@sequelize/sqlite3'; import { Attribute, AutoIncrement, BelongsTo, BelongsToMany, CreatedAt, Default, HasMany, NotNull, PrimaryKey, Unique, UpdatedAt } from "@sequelize/core/decorators-legacy"; @@ -6,6 +6,8 @@ import { Tag } from "./Tag"; import { PostTag } from "./PostTag"; import { Project } from "./Project"; import { Attachment } from "./Attachment"; +import { Bucket } from "./Bucket"; +import { UUID } from "crypto"; export class Post extends Model, InferCreationAttributes> { @@ -47,21 +49,29 @@ export class Post extends Model, InferCreationAttributes

Tag, { through: { model: ()=>PostTag, unique: false}, inverse: {as: 'taggedPosts'} }) declare postTags?:NonAttribute; - /** Defined by {@link Attachment.post} */ - declare Attachments:NonAttribute[]; + @BelongsToMany(()=>Bucket,{through: { model: ()=> PostBucket, unique: false}, inverse:{as: 'bucketPosts'}, foreignKey: 'postId', otherKey: 'bucketId'}) + declare postBuckets:NonAttribute; declare getUser: BelongsToGetAssociationMixin; + declare static associations: { user: Association; project: Association; + postBuckets: Association postTags: Association; }; } +export class PostBucket extends Model, InferCreationAttributes>{ + declare postId: number; + declare bucketId: UUID; +} const sequelize = new Sequelize({ dialect: SqliteDialect, storage: 'db.sqlite', - models: [Post] -}) \ No newline at end of file + models: [Post, PostBucket] +}) + +PostBucket.sync(); \ No newline at end of file