Skip to content

Commit

Permalink
Merge pull request #149 from codeanker/feature/123-anhänge-für-die-tn…
Browse files Browse the repository at this point in the history
…-bei-der-anmeldung

File Uploads
  • Loading branch information
superbarne authored Nov 23, 2024
2 parents 9173128 + f1a842b commit 8ccd5c6
Show file tree
Hide file tree
Showing 31 changed files with 1,005 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ typings/
reports
**/tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
uploads/
12 changes: 12 additions & 0 deletions api/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
"host": "MEILISEARCH_HOST",
"apiKey": "MEILISEARCH_KEY"
},
"fileDefaultProvider": "FILE_DEFAULT_PROVIDER",
"fileProviders": {
"LOCAL": {
"path":"FILE_PROVIDER_LOCAL_PATH"
},
"AZURE": {
"account": "FILE_PROVIDER_AZURE_ACCOUNT",
"accountKey": "FILE_PROVIDER_AZURE_ACCOUNT_KEY",
"container": "FILE_PROVIDER_AZURE_CONTAINER",
"folder": "FILE_PROVIDER_AZURE_FOLDER"
}
},
"tomtom": {
"apiKey": "TOMTOM_APIKEY"
},
Expand Down
12 changes: 12 additions & 0 deletions api/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
"host": "meilisearch:7700",
"apiKey": "xbAPupQKhkKvF176vE2JxAdPGpmWVu251Hldn6K4Z6Y"
},
"fileDefaultProvider": "LOCAL",
"fileProviders": {
"LOCAL": {
"path":"../uploads"
},
"AZURE": {
"account": "",
"accountKey": "",
"container": "",
"folder": ""
}
},
"tomtom": {
"apiKey": ""
},
Expand Down
2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"seed": "tsx prisma/seeders/index.ts"
},
"dependencies": {
"@azure/storage-blob": "^12.17.0",
"@faker-js/faker": "^8.4.1",
"@koa/cors": "^5.0.0",
"@koa/router": "^12.0.1",
Expand All @@ -45,6 +46,7 @@
"koa-session": "^6.4.0",
"koa-static": "^5.0.0",
"meilisearch": "^0.37.0",
"mime-types": "^2.1.35",
"mjml": "^4.15.3",
"prom-client": "^15.0.0",
"superjson": "^2.2.1",
Expand Down
42 changes: 42 additions & 0 deletions api/prisma/migrations/20241118163314_init_file/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "FileProvider" AS ENUM ('LOCAL', 'AZURE');

-- CreateTable
CREATE TABLE "File" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"uploaded" BOOLEAN NOT NULL DEFAULT false,
"uploadedAt" TIMESTAMP(3),
"provider" "FileProvider" NOT NULL,
"key" TEXT NOT NULL,
"filename" TEXT,
"mimetype" TEXT,

CONSTRAINT "File_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "UnterveranstaltungDocument" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"unterveranstaltungId" INTEGER NOT NULL,
"fileId" TEXT NOT NULL,

CONSTRAINT "UnterveranstaltungDocument_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "File_id_key" ON "File"("id");

-- CreateIndex
CREATE UNIQUE INDEX "File_key_key" ON "File"("key");

-- CreateIndex
CREATE UNIQUE INDEX "UnterveranstaltungDocument_fileId_key" ON "UnterveranstaltungDocument"("fileId");

-- AddForeignKey
ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_unterveranstaltungId_fkey" FOREIGN KEY ("unterveranstaltungId") REFERENCES "Unterveranstaltung"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "UnterveranstaltungDocument" ADD CONSTRAINT "UnterveranstaltungDocument_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
16 changes: 16 additions & 0 deletions api/prisma/schema/File.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
enum FileProvider {
LOCAL
AZURE
}

model File {
id String @id @unique @default(uuid())
createdAt DateTime @default(now())
uploaded Boolean @default(false)
uploadedAt DateTime?
provider FileProvider
key String @unique()
filename String?
mimetype String?
UnterveranstaltungDocument UnterveranstaltungDocument?
}
11 changes: 11 additions & 0 deletions api/prisma/schema/Unterveranstaltung.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ model Unterveranstaltung {
bedingungen String?
type UnterveranstaltungType @default(GLIEDERUNG)
customFields CustomField[]
documents UnterveranstaltungDocument[]
}

model UnterveranstaltungDocument {
id Int @id @default(autoincrement())
name String
description String?
unterveranstaltungId Int
unterveranstaltung Unterveranstaltung? @relation(fields: [unterveranstaltungId], references: [id], onDelete: Cascade)
file File @relation(fields: [fileId], references: [id])
fileId String @unique
}
30 changes: 30 additions & 0 deletions api/src/azureStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob'

import config from './config'

const isAzureConfigured =
config.fileProviders.AZURE.account !== '' &&
config.fileProviders.AZURE.accountKey !== '' &&
config.fileProviders.AZURE.container !== ''

async function init() {
if (!isAzureConfigured) return null
const account = config.fileProviders.AZURE.account
const accountKey = config.fileProviders.AZURE.accountKey
const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey)

const blobServiceClient = new BlobServiceClient(`https://${account}.blob.core.windows.net`, sharedKeyCredential)

// Check if container exists, if not create it
const containerClient = blobServiceClient.getContainerClient(config.fileProviders.AZURE.container)
if (!(await containerClient.exists())) await containerClient.create()
return blobServiceClient
}

export let azureStorage: BlobServiceClient | null = null

init()
.then((blobServiceClient) => (azureStorage = blobServiceClient))
.catch((e) => {
console.error('Failed to initialize Azure Blob Storage', e)
})
14 changes: 14 additions & 0 deletions api/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'node:path'
import { fileURLToPath } from 'url'

import { FileProvider } from '@prisma/client'
import config from 'config'
import { z } from 'zod'

Expand Down Expand Up @@ -43,6 +44,19 @@ export const configSchema = z.strictObject({
host: z.string(),
apiKey: z.string(),
}),
fileDefaultProvider: z.nativeEnum(FileProvider),
fileProviders: z.strictObject({
LOCAL: z.strictObject({
path: z.string(),
}),
AZURE: z.strictObject({
account: z.string(),
accountKey: z.string(),
container: z.string(),
folder: z.string(),
}),
}),

tomtom: z.strictObject({
apiKey: z.string(),
}),
Expand Down
34 changes: 34 additions & 0 deletions api/src/middleware/downloadFileLocal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as fs from 'fs'
import * as path from 'path'

import type { Middleware } from 'koa'
import mime from 'mime-types'

import config from '../config'
import prisma from '../prisma'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const downloadFileLocal: Middleware = async function (ctx, next) {
const fileId = ctx.params.id
const file = await prisma.file.findFirst({
where: {
id: fileId,
},
})
if (file === null) {
ctx.response.status = 404
return
}

if (file.provider !== 'LOCAL') {
ctx.response.status = 404
return
}

const uploadDir = path.join(process.cwd(), config.fileProviders.LOCAL.path)
const mimetype = file.mimetype ?? 'application/octet-stream'
const filename = file.filename ?? `${file.id}.${mime.extension(mimetype)}`
ctx.set('Content-disposition', `attachment; filename=${filename}`)
ctx.set('Content-type', mimetype)
ctx.response.body = fs.createReadStream(uploadDir + '/' + file.key)
}
4 changes: 4 additions & 0 deletions api/src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type Router from 'koa-router'

import { downloadFileLocal } from './downloadFileLocal'
import { importAnmeldungen } from './importAnmeldungen'
import { uploadFileLocal } from './uploadFileLocal'

export default function addMiddlewares(router: Router) {
router.post('/upload/anmeldungen', async (ctx, next) => {
return await importAnmeldungen(ctx, next)
})
router.post('/upload/file/LOCAL/:id', uploadFileLocal)
router.get('/download/file/LOCAL/:id', downloadFileLocal)
}
88 changes: 88 additions & 0 deletions api/src/middleware/uploadFileLocal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as fs from 'fs/promises'
import * as path from 'path'

import type { Middleware } from 'koa'

import config from '../config'
import prisma from '../prisma'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const uploadFileLocal: Middleware = async function (ctx, next) {
const fileId = ctx.params.id
const file = await prisma.file.findFirst({
where: {
id: fileId,
},
})
if (file === null) {
ctx.response.status = 400
ctx.response.body = { error: `File with id '${fileId}' not found` }
return
}

if (file.uploaded) {
ctx.response.status = 400
ctx.response.body = { error: `File with id '${fileId}' already uploaded` }
return
}

if (file.provider !== 'LOCAL') {
ctx.response.status = 400
ctx.response.body = { error: `File provider is '${file.provider}'. This endpoint is for LOCAL` }
return
}

const uploadDir = path.join(process.cwd(), config.fileProviders.LOCAL.path)
try {
await checkLocalUploadFolder(uploadDir)
} catch (e) {
console.error('Error while creating upload-directory\n', e)
ctx.response.status = 500
ctx.response.body = {
error: 'Something went wrong during creation of upload-directory',
}
return
}

const fileData = ctx.request.files?.file
if (!fileData || Array.isArray(fileData)) {
ctx.response.status = 400
ctx.response.body = {
error: 'No or Invalid File provided',
}
return
}

try {
await fs.copyFile(fileData.filepath, uploadDir + '/' + file.key)
} catch (e) {
console.error('Error while copy to upload-directory\n', e)
ctx.response.status = 500
ctx.response.body = {
error: 'Something went wrong during copy to upload-directory',
}
return
}

await prisma.file.update({
where: { id: fileId },
data: {
mimetype: fileData.mimetype ?? 'application/octet-stream',
filename: fileData.originalFilename ?? undefined,
uploaded: true,
uploadedAt: new Date(),
},
})

ctx.response.status = 201
ctx.response.body = { uploaded: true }
}

async function checkLocalUploadFolder(uploadDir: string) {
try {
await fs.stat(uploadDir)
} catch (e: any) {
if (e.code === 'ENOENT') await fs.mkdir(uploadDir, { recursive: true })
else throw e
}
}
12 changes: 12 additions & 0 deletions api/src/services/file/file.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable prettier/prettier */ // Prettier ignored is because this file is generated
import { mergeRouters } from '../../trpc'

import { fileCreateProcedure } from './fileCreate'
import { fileGetUrlActionProcedure } from './fileGetUrl'
// Import Routes here - do not delete this line

export const fileRouter = mergeRouters(
fileCreateProcedure.router,
fileGetUrlActionProcedure.router,
// Add Routes here - do not delete this line
)
Loading

0 comments on commit 8ccd5c6

Please sign in to comment.