Skip to content

Commit

Permalink
Merge pull request #142 from RA341/assignment-up
Browse files Browse the repository at this point in the history
Assignment up
  • Loading branch information
jessehartloff authored Oct 15, 2024
2 parents 4e5c1cd + d8db9ae commit c36c7e0
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 196 deletions.
8 changes: 4 additions & 4 deletions devU-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
"version": "1.0.0",
"description": "DevU API",
"scripts": {
"start": "ts-node-dev src/index.ts",
"update-shared": "npm update devu-shared-modules",
"start": "npm run migrate && ts-node-dev src/index.ts",
"migrate": "npm run typeorm -- migration:run -d src/database.ts",
"update-shared": "cd ../devU-shared && npm run build-local && npm i",
"typeorm": "typeorm-ts-node-commonjs",
"test": "jest --passWithNoTests",
"clean": "rimraf build/*",
"lint": "tsc",
"build": "npm-run-all clean lint",
"format": "prettier --write \"./**/*.{js,ts,json,md}\"",
"pre-commit": "lint-staged",
"generate-config": "docker run --pull always -v $(pwd)/config:/config --user $(id -u):$(id -g) --rm ubautograding/devtools /generateConfig.sh config/default.yml",
"populate-db": "ts-node-dev ./scripts/populate-db.ts",
"tango": "ts-node-dev ./src/tango/tests/tango.endpoint.test.ts",
"api-services": "docker compose -f ../docker-compose.yml --profile dev-api up -d",
"api-services": "docker compose -f ../docker-compose.yml --profile dev-api up",
"api-services-stop": "docker compose -f ../docker-compose.yml --profile dev-api stop"
},
"lint-staged": {
Expand Down
77 changes: 75 additions & 2 deletions devU-api/src/entities/assignment/assignment.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import AssignmentService from './assignment.service'
import { GenericResponse, NotFound, Updated } from '../../utils/apiResponse.utils'

import { serialize } from './assignment.serializer'
import { BucketNames, downloadFile, uploadFile } from '../../fileStorage'
import { generateFilename } from '../../utils/fileUpload.utils'

export async function detail(req: Request, res: Response, next: NextFunction) {
try {
Expand All @@ -22,6 +24,40 @@ export async function detail(req: Request, res: Response, next: NextFunction) {
}
}

export async function handleAttachmentLink(req: Request, res: Response, next: NextFunction) {
try {
const bucketName = BucketNames.ASSIGNMENTSATTACHMENTS
const courseId = parseInt(req.params.courseId)
const fileName = req.params.filename
const assignmentId = parseInt(req.params.assignmentId)

if (!courseId) return res.status(400).json('Bucket not found')
if (!fileName) return res.status(400).json('File name not found')
if (!assignmentId) return res.status(400).json('Assignment id not found')

const assignment = await AssignmentService.retrieve(assignmentId, courseId)
if (!assignment) return res.status(404).json('Assignment not found')

const ind = assignment.attachmentsHashes.findIndex(value => {
return value == fileName
})
if (ind == -1) return res.status(404).json('File not found')

const file = assignment.attachmentsHashes[ind]
const name = assignment.attachmentsFilenames[ind]

const buffer = await downloadFile(bucketName, file)

res.setHeader('Content-Disposition', `attachment; filename="${name}"`)
res.setHeader('Content-Type', 'application/octet-stream')
res.setHeader('Content-Length', buffer.length)
res.send(buffer)
} catch (error) {
console.error('Error retrieving file:', error)
res.status(500).send('Error retrieving file')
}
}

export async function getByCourse(req: Request, res: Response, next: NextFunction) {
try {
const courseId = parseInt(req.params.courseId)
Expand All @@ -34,6 +70,7 @@ export async function getByCourse(req: Request, res: Response, next: NextFunctio
next(err)
}
}

export async function getReleased(req: Request, res: Response, next: NextFunction) {
try {
const courseId = parseInt(req.params.courseId)
Expand All @@ -47,21 +84,57 @@ export async function getReleased(req: Request, res: Response, next: NextFunctio
}
}


async function processFiles(req: Request) {
let fileHashes: string[] = []
let fileNames: string[] = []

// save files
if (req.files) {
console.log()
if (Array.isArray(req.files)) {
for (let index = 0; index < req.files.length; index++) {
const item = req.files[index]
const filename = generateFilename(item.originalname, item.size)
await uploadFile(BucketNames.ASSIGNMENTSATTACHMENTS, item, filename)
fileHashes.push(filename)
fileNames.push(item.originalname)
}
} else {
console.warn(`Files where not in array format ${req.files}`)
}
} else {
console.warn(`No files where processed`)
}

return { fileHashes, fileNames }
}

export async function post(req: Request, res: Response, next: NextFunction) {
try {
const { fileNames, fileHashes } = await processFiles(req)

req.body['attachmentsFilenames'] = fileNames
req.body['attachmentsHashes'] = fileHashes

const assignment = await AssignmentService.create(req.body)
const response = serialize(assignment)

res.status(201).json(response)
} catch (err) {
if (err instanceof Error) {
res.status(400).json(new GenericResponse(err.message))
res.status(400).json(new GenericResponse(err.message))
}
}
}

export async function put(req: Request, res: Response, next: NextFunction) {
try {
const { fileNames, fileHashes } = await processFiles(req)

req.body['attachmentsFilenames'] = fileNames
req.body['attachmentsHashes'] = fileHashes

req.body.id = parseInt(req.params.assignmentId)
const results = await AssignmentService.update(req.body)

Expand All @@ -86,4 +159,4 @@ export async function _delete(req: Request, res: Response, next: NextFunction) {
}
}

export default { detail, post, put, _delete, getByCourse, getReleased }
export default { detail, post, put, _delete, getByCourse, getReleased, handleAttachmentLink }
14 changes: 14 additions & 0 deletions devU-api/src/entities/assignment/assignment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export default class AssignmentModel {
* type: string
* format: date-time
* description: Must be in ISO 8601 format
* fileHashes:
* type: string
* array: true
* description: filename hashes of stored attachments use this to retrieve and query attachments, matches the index of the fileNames, i.e. filename[i] is the name of hash[i]
* fileNames:
* type: string
* array: true
* description: filenames of stored attachments, matches the index of the fileHashes, i.e. filename[i] is the name of hash[i]
*/

@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -95,4 +103,10 @@ export default class AssignmentModel {

@DeleteDateColumn({ name: 'deleted_at' })
deletedAt?: Date

@Column({ name: 'attachmentsHashes', array: true, default: [], type: 'text' })
attachmentsHashes: string[]

@Column({ name: 'attachmentsFilenames', array: true, default: [], type: 'text', nullable: false })
attachmentsFilenames: string[]
}
45 changes: 43 additions & 2 deletions devU-api/src/entities/assignment/assignment.router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Libraries
import multer from 'multer'
import express from 'express'

// Middleware
Expand All @@ -10,6 +11,7 @@ import AssignmentsController from './assignment.controller'
import { isAuthorized, isAuthorizedByAssignmentStatus } from '../../authorization/authorization.middleware'

const Router = express.Router({ mergeParams: true })
const upload = multer({ limits: { fileSize: 1024 * 1024 * 5 } }) // 5MB file size limit

/**
* @swagger
Expand Down Expand Up @@ -51,6 +53,42 @@ Router.get('/released', isAuthorized('assignmentViewReleased'), AssignmentsContr
*/
Router.get('/', isAuthorized('assignmentViewAll'), AssignmentsController.getByCourse)

/**
* @swagger
* /course/:courseId/assignments/attachments/{assignmentId}/{filename}:
* get:
* summary: Retrieve an attachment for assignment
* tags:
* - Assignments
* responses:
* '200':
* description: OK
* '400':
* description: Missing the 'assignment id' or 'course id' or 'filename'
* '404':
* description: file not found or not part of the assigment
* parameters:
* - name: courseId
* in: path
* description: Enter course id
* required: true
* schema:
* type: integer
* - name: assignmentId
* in: path
* description: Enter assignment id
* required: true
* schema:
* type: integer
* - name: filename
* in: path
* description: Enter filename hash
* required: true
* schema:
* type: string
*/
Router.get('/attachments/:assignmentId/:filename', isAuthorizedByAssignmentStatus, AssignmentsController.handleAttachmentLink)

/**
* @swagger
* /course/:courseId/assignments/{id}:
Expand Down Expand Up @@ -100,7 +138,9 @@ Router.get('/:assignmentId', asInt('assignmentId'), isAuthorizedByAssignmentStat
* schema:
* $ref: '#/components/schemas/Assignment'
*/
Router.post('/', isAuthorized('assignmentEditAll'), validator, AssignmentsController.post)


Router.post('/', isAuthorized('assignmentEditAll'), upload.array('files', 5), validator, AssignmentsController.post)

/**
* @swagger
Expand Down Expand Up @@ -134,8 +174,9 @@ Router.put(
'/:assignmentId',
isAuthorized('assignmentEditAll'),
asInt('assignmentId'),
upload.array('files', 5),
validator,
AssignmentsController.put
AssignmentsController.put,
)

/**
Expand Down
2 changes: 2 additions & 0 deletions devU-api/src/entities/assignment/assignment.serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ export function serialize(assignment: AssignmentModel): Assignment {
disableHandins: assignment.disableHandins,
createdAt: assignment.createdAt.toISOString(),
updatedAt: assignment.updatedAt.toISOString(),
attachmentsHashes: assignment.attachmentsHashes,
attachmentsFilenames: assignment.attachmentsFilenames
}
}
4 changes: 4 additions & 0 deletions devU-api/src/entities/assignment/assignment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export async function update(assignment: Assignment) {
maxFileSize,
maxSubmissions,
disableHandins,
attachmentsHashes,
attachmentsFilenames,
} = assignment

if (!id) throw new Error('Missing Id')
Expand All @@ -37,6 +39,8 @@ export async function update(assignment: Assignment) {
maxFileSize,
maxSubmissions,
disableHandins,
attachmentsHashes,
attachmentsFilenames
})
}

Expand Down
3 changes: 2 additions & 1 deletion devU-api/src/fileStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum BucketNames {
GRADERS = 'graders',
SUBMISSIONS = 'submissions',
MAKEFILES = 'makefiles',
ASSIGNMENTSATTACHMENTS = 'assignmentattachments'
}

const minioConfiguration: Minio.ClientOptions = {
Expand Down Expand Up @@ -43,7 +44,7 @@ export async function initializeMinio(inputBucketName?: string) {

export async function uploadFile(bucketName: string, file: Express.Multer.File, filename: string): Promise<string> {
try {
const objInfo = await minioClient.putObject(bucketName, filename, file.buffer)
const objInfo = await minioClient.putObject(bucketName, filename, file.buffer, file.size)
return objInfo.etag
} catch (err: Error | any) {
if (err) {
Expand Down
16 changes: 16 additions & 0 deletions devU-api/src/migration/1728881406096-assignmentAttachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AssignmentAttachment1728881406096 implements MigrationInterface {
name = 'AssignmentAttachment1728881406096'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assignments" ADD "attachmentsHashes" text array NOT NULL DEFAULT '{}'`);
await queryRunner.query(`ALTER TABLE "assignments" ADD "attachmentsFilenames" text array NOT NULL DEFAULT '{}'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assignments" DROP COLUMN "attachmentsFilenames"`);
await queryRunner.query(`ALTER TABLE "assignments" DROP COLUMN "attachmentsHashes"`);
}

}
Loading

0 comments on commit c36c7e0

Please sign in to comment.