Skip to content

Commit

Permalink
(add) polygon upload session
Browse files Browse the repository at this point in the history
  • Loading branch information
shine00chang committed Oct 5, 2024
1 parent 3e2878b commit a0ebad9
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 79 deletions.
52 changes: 43 additions & 9 deletions packages/api-server/src/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ <h2>Problems</h2>
<input type="text" id="problem-input" placeholder="input"><br>
<input type="text" id="problem-output" placeholder="output"><br>
<button id='problem-add'>add</button> <br>
<button id='problem-polygon'>polygon add</button> (zipped package)
<input type="file" id="problem-polygon-input"> <br>
<button id='problem-get'>get</button> <br>

<div id='problem-list'>
Expand Down Expand Up @@ -129,7 +131,7 @@ <h2>Testcases</h2>
</div>

<script>
const uploadURL = 'http://13.93.218.61:8001';
const uploadURL = 'http://localhost:8001';

let getUserId = _ => undefined;

Expand Down Expand Up @@ -300,24 +302,56 @@ <h2>Testcases</h2>
.catch(console.error);
}

document.getElementById('problem-polygon').onclick = async _ => {

console.log('problem-polygon');

if (getDomainId() === undefined) return alert('no domain selected');

if (!document.getElementById('problem-polygon-input').files[0])
return alert('no polygon file selected');

const { uploadId } = await betterFetch(`/v1/domains/${getDomainId()}/problems/polygon-upload-session`);

const formData = new FormData();
formData.append('package', document.getElementById('problem-polygon-input').files[0]);

const res = await betterFetch(uploadURL + `/polygon/${uploadId}`, {
method: "POST",
body: formData
})
.then(res => alert('package uploaded'))
}

document.getElementById('problem-get').onclick = async _ => {

console.log('problem-get');

if (getDomainId() === undefined) return alert('no domain selected');

const res = await betterFetch(`/v1/domains/${getDomainId()}/problems/`)
const problems = await betterFetch(`/v1/domains/${getDomainId()}/problems/`)

let html = '';
for (const { name, id } of res) {
html += `
<div>
<input type='radio' name='problem-radio' value='${id}'>
<label for='${id}'><b>${name}</b>: (<i>${id}</i>)</label>
for (const { name, id, context, inputFormat, outputFormat, constraints } of problems) {

const problemHTML = `
${constraints.time}ms <br>
${constraints.memory}MB <br>
<hr>
${context}<br>
${inputFormat}<br>
${outputFormat}<br>
<hr>`;
html += `
<details>
<summary>
<input type='radio' name='problem-radio' value='${id}'>
<label for='${id}'><b>${name}</b> (<i>${id}</i>)</label>
</summary>
${problemHTML}
</div>`
}

document.getElementById('problem-list').innerHTML = html;
document.getElementById('problem-list').innerHTML = html;
}

document.getElementById('series-add').onclick = async _ => {
Expand Down
29 changes: 20 additions & 9 deletions packages/api-server/src/client.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
margin:40px auto;max-width:700px;line-height:1.6;font-size:18px;color:#444;padding:0 10px;
font-family: 'Poppins';
}
b{font-weight: 900}
h1,h2,h3{line-height:1.2}
input,div,button { margin: 0.2rem }
input {
Expand Down Expand Up @@ -32,7 +33,6 @@
cursor: pointer;
padding: 5px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
vertical-align: middle;
border: 1px solid;
Expand Down Expand Up @@ -261,17 +261,28 @@ <h2>Ranklist</h2>

if (getContestId() === undefined) return alert('no contest selected');

const res = await betterFetch(`/v1/contests/${getContestId()}/problems/`)

if (res.error)
return console.error(res.message);
const { problems } = await betterFetch(`/v1/contests/${getContestId()}/problems/`)

let html = '';
for (const { name, id } of res.problems) {
for (const { name, id } of problems) {

const problem = await betterFetch(`/v1/contests/${getContestId()}/problems/${id}`);

const problemHTML = `
${problem.constraints.time}ms <br>
${problem.constraints.memory}MB <br>
<hr>
${problem.context}<br>
${problem.inputFormat}<br>
${problem.outputFormat}<br>
<hr>`;
html += `
<div>
<input type='radio' name='problem-radio' value='${id}'>
<label for='${id}'><b>${name}</b> (<i>${id}</i>)</label>
<details>
<summary>
<input type='radio' name='problem-radio' value='${id}'>
<label for='${id}'><b>${name}</b> (<i>${id}</i>)</label>
</summary>
${problemHTML}
</div>`
}

Expand Down
26 changes: 25 additions & 1 deletion packages/api-server/src/routes/v1/domain.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { type FastifyTypeBox } from '../../types.js'
import { createDomainProblem, deleteDomainProblem, fetchDomainProblems, updateDomainProblem } from '../../services/problem.services.js'
import { fetchDomainProblem } from '@argoncs/common'
import { createTestingSubmission } from '../../services/submission.services.js'
import { createUploadSession } from '../../services/testcase.services.js'
import { createPolygonUploadSession, createUploadSession } from '../../services/testcase.services.js'
import { UnauthorizedError, badRequestSchema, forbiddenSchema, methodNotAllowedSchema, notFoundSchema, unauthorizedSchema } from 'http-errors-enhanced'
import { createContest, createContestSeries, fetchDomainContestSeries, fetchDomainContests } from '../../services/contest.services.js'
import { userAuthHook } from '../../hooks/authentication.hooks.js'
Expand Down Expand Up @@ -309,6 +309,30 @@ async function domainProblemRoutes (problemRoutes: FastifyTypeBox): Promise<void
await reply.status(200).send({ uploadId })
}
)

problemRoutes.get(
'/polygon-upload-session',
{
schema: {
response: {
200: Type.Object({ uploadId: Type.String() }),
400: badRequestSchema,
401: unauthorizedSchema,
403: forbiddenSchema,
404: notFoundSchema
},
params: Type.Object({ domainId: Type.String() })
},
onRequest: [userAuthHook, problemRoutes.auth([
[hasDomainPrivilege(['problem.manage'])]
]) as any]
},
async (request, reply) => {
const { domainId } = request.params
const { uploadId } = await createPolygonUploadSession({ domainId })
await reply.status(200).send({ uploadId })
}
)
}

async function domainContestRoutes (contestRoutes: FastifyTypeBox): Promise<void> {
Expand Down
6 changes: 3 additions & 3 deletions packages/api-server/src/services/problem.services.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
type NewProblem,
type Problem
} from '@argoncs/types'
} from '@argoncs/types' /*=*/
import { NotFoundError } from 'http-errors-enhanced'
import { mongoClient, domainProblemCollection, submissionCollection, testcaseUploadCollection } from '@argoncs/common'
import { mongoClient, domainProblemCollection, submissionCollection, uploadSessionCollection } from '@argoncs/common'
import { testcaseExists } from './testcase.services.js'

import { nanoid } from 'nanoid'
Expand Down Expand Up @@ -43,7 +43,7 @@ export async function deleteDomainProblem ({ problemId, domainId }: { problemId:
throw new NotFoundError('Problem not found')
}

await testcaseUploadCollection.deleteMany({ problemId })
await uploadSessionCollection.deleteMany({ problemId })
await submissionCollection.deleteMany({ problemId })
})
} finally {
Expand Down
10 changes: 8 additions & 2 deletions packages/api-server/src/services/testcase.services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NotFoundError } from 'http-errors-enhanced'
import { fetchDomainProblem, minio, testcaseUploadCollection } from '@argoncs/common'
import { fetchDomainProblem, minio, uploadSessionCollection } from '@argoncs/common'

import path = require('node:path')
import { nanoid } from 'nanoid'
Expand All @@ -20,6 +20,12 @@ export async function testcaseExists ({ problemId, domainId, filename, versionId
export async function createUploadSession ({ problemId, domainId }: { problemId: string, domainId: string }): Promise<{ uploadId: string }> {
const id = nanoid(32)
await fetchDomainProblem({ problemId, domainId }) // Could throw not found
await testcaseUploadCollection.insertOne({ id, problemId, domainId, createdAt: (new Date()).getTime() })
await uploadSessionCollection.insertOne({ id, problemId, domainId, createdAt: (new Date()).getTime() })
return { uploadId: id }
}

export async function createPolygonUploadSession ({ domainId }: { domainId: string }): Promise<{ uploadId: string }> {
const id = nanoid(32)
await uploadSessionCollection.insertOne({ id, domainId, polygon: true, createdAt: (new Date()).getTime() })
return { uploadId: id }
}
8 changes: 4 additions & 4 deletions packages/common/src/connections/mongodb.connections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ConetstProblemList, type Contest, type ContestProblem, type Submission, type Domain, type EmailVerification, type Problem, type Team, type TeamInvitation, type TestcaseUpload, type User, type UserPrivateSession, type TeamScore, type ContestSeries } from '@argoncs/types'
import { type ConetstProblemList, type Contest, type ContestProblem, type Submission, type Domain, type EmailVerification, type Problem, type Team, type TeamInvitation, type User, type UserPrivateSession, type TeamScore, type ContestSeries, type UploadSession } from '@argoncs/types'
import { MongoClient, type IndexSpecification, type CreateIndexesOptions, type Db, type Collection } from 'mongodb'

interface Index {
Expand Down Expand Up @@ -59,7 +59,7 @@ const collections: CollectionIndex[] = [
]
},
{
name: 'testcaseUploads',
name: 'uploadSessions',
indexes: [
{ keys: { id: 1 }, options: { unique: true } },
{ keys: { problemId: 1 } },
Expand Down Expand Up @@ -125,7 +125,7 @@ export let domainProblemCollection: Collection<Problem>
export let submissionCollection: Collection<Submission>
export let sessionCollection: Collection<UserPrivateSession>
export let emailVerificationCollection: Collection<EmailVerification>
export let testcaseUploadCollection: Collection<TestcaseUpload>
export let uploadSessionCollection: Collection<UploadSession>
export let contestCollection: Collection<Contest>
export let teamCollection: Collection<Team>
export let teamInvitationCollection: Collection<TeamInvitation>
Expand Down Expand Up @@ -156,7 +156,7 @@ export async function connectMongoDB (url: string): Promise<void> {

submissionCollection = mongoDB.collection('submissions')

testcaseUploadCollection = mongoDB.collection('testcaseUploads')
uploadSessionCollection = mongoDB.collection('uploadSessions')

contestCollection = mongoDB.collection('contests')
contestProblemCollection = mongoDB.collection('contestProblems')
Expand Down
5 changes: 3 additions & 2 deletions packages/types/src/types/problem.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ export type Problem = Static<typeof ProblemSchema>
export const ContestProblemSchema = Type.Intersect([ProblemSchema, Type.Object({ contestId: Type.String(), obsolete: Type.Boolean() })])
export type ContestProblem = Static<typeof ContestProblemSchema>

export interface TestcaseUpload {
export interface UploadSession {
id: string
problemId: string
problemId?: string
polygon?: boolean
domainId: string
createdAt: number
}
60 changes: 60 additions & 0 deletions packages/upload-server/src/routes/polygon.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { consumePolygonUploadSession, consumeUploadSession, uploadTestcase } from '../services/testcase.services.js'
import { uploadPolygon } from '../services/polygon.services.js'

import { Type } from '@sinclair/typebox'
import multipart, { type MultipartFile } from '@fastify/multipart'
import { type FastifyTypeBox } from '../types.js' /*=*/
import { BadRequestError, badRequestSchema, PayloadTooLargeError, unauthorizedSchema } from 'http-errors-enhanced'

export async function polygonRoutes (routes: FastifyTypeBox): Promise<void> {
await routes.register(multipart.default, {
limits: {
fileSize: 20971520,
files: 1
}
})

/**
* Uploads a zipped Polygon package.
* Unzips and parses the package, and update the problem.
*/
routes.post(
'/:uploadId',
{
schema: {
params: Type.Object({ uploadId: Type.String() }),
response: {
201: Type.Object({ problemId: Type.String() }),
400: badRequestSchema,
401: unauthorizedSchema,
413: Type.Object({ statusCode: Type.Number(), error: Type.String(), message: Type.String() })
}
}
},
async (request, reply) => {
const { uploadId } = request.params
const { domainId } = await consumePolygonUploadSession(uploadId)

try {
const archive: MultipartFile | undefined = await request.file()
if (archive === undefined) {
throw new BadRequestError('No file found in request')
}

const problemId = await uploadPolygon({ domainId, archive })

return await reply.status(201).send({ problemId })
} catch (err) {
if (err instanceof routes.multipartErrors.InvalidMultipartContentTypeError) {
throw new BadRequestError('Request must be multipart')
} else if (err instanceof routes.multipartErrors.FilesLimitError) {
throw new PayloadTooLargeError('Too many files in one request')
} else if (err instanceof routes.multipartErrors.RequestFileTooLargeError) {
throw new PayloadTooLargeError('Testcase too large to be processed')
} else {
throw err
}
}
}
)
}
32 changes: 0 additions & 32 deletions packages/upload-server/src/routes/testcase.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,36 +54,4 @@ export async function testcaseRoutes (routes: FastifyTypeBox): Promise<void> {
}
}
)

/**
* Uploads a zipped Polygon package.
* Unzips and parses the package, and update the problem.
*/
routes.post(
'/:uploadId/polygon',
{
schema: {
params: Type.Object({ uploadId: Type.String() }),
response: {
201: Type.Object({ problemId: Type.String() }),
400: badRequestSchema,
401: unauthorizedSchema,
413: Type.Object({ statusCode: Type.Number(), error: Type.String(), message: Type.String() })
}
}
},
async (request, reply) => {
const { uploadId } = request.params
const { domainId, problemId } = await consumeUploadSession(uploadId)

const archive = await request.file()
if (archive === undefined) {
throw new BadRequestError('No file found in request')
}

await uploadPolygon(domainId, problemId, archive)

return await reply.status(201).send({ problemId })
}
)
}
Loading

0 comments on commit a0ebad9

Please sign in to comment.