diff --git a/packages/admin/uploading-charts.md b/packages/admin/uploading-charts.md new file mode 100644 index 0000000..50f7186 --- /dev/null +++ b/packages/admin/uploading-charts.md @@ -0,0 +1,87 @@ +# Process for Uploading Charts + +1. Find a bms bundle/zip ie. `p_stands_for_party_newbga_ogg.zip` +2. Upload zip to BackBlaze B2 storage bucket +3. Open the admin dashboard for bms-repository +4. Unzip the bms bundle/zip on your local machine +5. For each bms file in the unzipped folder: + 1. Copy the zip friendly url from BackBlaze B2 + 2. Create a new chart in the admin dashboard + 3. Paste the zip friendly url into the `resourceUri` field + 4. Set the `md5` and `sha256` fields to the md5 and sha256 of the bms file + 5. Set the `name` field to the name of the chart + difficulty + - ie. `"P" stands for "Party!" [Another]` + +## Script for generating chart JSON +Run in the freshly unzipped folder + +```shell +#!/bin/bash + +FILES=($(find . -name "*.bm*")); + + +JSON="[" +for CHART in "${FILES[@]}" +do + MD5=$(md5sum "$CHART" | cut -d ' ' -f 1) + SHA256=$(sha256sum "$CHART" | cut -d ' ' -f 1) + TITLE=$(cat "$CHART" | grep "#TITLE" | sed -e s/^.*\#TITLE// | xargs | tr -d '\r') + UUID=$(uuidgen) + + JSON+=$(jq -n \ + --arg chartId "$UUID" \ + --arg md5 "$MD5" \ + --arg sha256 "$SHA256" \ + --arg name "$TITLE" \ + --arg resourceUri "$1" \ + '. += $ARGS.named') +done + +echo "$JSON]" | sed -e s/\}\{/},{/g | jq # Lazy, couldn't get JQ append working lol +``` + +First argument is the upload url +```shell +./export.sh https://example.com/zips/myzip.zip +``` + +While authenticated as an admin user, hit the `bulk` endpoint with the generated JSON after confirming that it is valid. + + +```shell +curl --request POST \ + --url https:///chart/admin/bulk \ + --header 'Authorization: Bearer Token' \ + --header 'Content-Type: application/json' \ + --data '[ + { + "chartId": "9923c511-355c-4ed8-9551-4b31fec45560", + "md5": "8d80d1732fa4c81dcbd4ddfd1673d949", + "sha256": "b796cdef4fa8865035d282be275cbda878203359c8ba630ac9ba619b52322346", + "name": "P stands for Party! [Hyper]", + "resourceUri": "https://f002.backblazeb2.com/file/bms-chart-storage-staging/p_stands_for_party_newbga_ogg.zip" + }, + { + "chartId": "4684b205-d1d9-4bcc-ace3-47e044988ff0", + "md5": "b81eea46f8600fabab3e3f6bce2a2cc7", + "sha256": "806c56cbb413d390c84a8d49b9888fecaac4571d5a2337b450ec23f28b10c725", + "name": "P stands for Party! [Another]", + "resourceUri": "https://f002.backblazeb2.com/file/bms-chart-storage-staging/p_stands_for_party_newbga_ogg.zip" + }, + { + "chartId": "ce9af400-4726-49ed-9ee2-13652bd98410", + "md5": "b963e1f212e3783cfd566b59b5cb472d", + "sha256": "17588424eea6479ed90dec7d7ffa1991bc049fd4c2ea218a1408f8febed64121", + "name": "P stands for Party! [Beginner]", + "resourceUri": "https://f002.backblazeb2.com/file/bms-chart-storage-staging/p_stands_for_party_newbga_ogg.zip" + }, + { + "chartId": "9834898f-c0ba-4a4f-a0b9-ac701e8a6f3a", + "md5": "c6c5e5e20b53983ba59252ddcc205b5b", + "sha256": "a60bdb0e6d2cd9d54c53decc33932b2eee7cd15966e0dd0666c368ff58070a3a", + "name": "P stands for Party! [Normal]", + "resourceUri": "https://f002.backblazeb2.com/file/bms-chart-storage-staging/p_stands_for_party_newbga_ogg.zip" + } +]' +``` diff --git a/packages/api/src/baseblocks/chart/chart-admin-api.ts b/packages/api/src/baseblocks/chart/chart-admin-api.ts index 2564300..2e2af4b 100644 --- a/packages/api/src/baseblocks/chart/chart-admin-api.ts +++ b/packages/api/src/baseblocks/chart/chart-admin-api.ts @@ -35,6 +35,32 @@ app.post('/chart/admin', [ }, ]); +app.post('/chart/admin/bulk', [ + isAdmin, + async (req: RequestContext, res: Response) => { + try { + const chartData = (req.body as Chart[]).map((chart) => ({ + md5: chart.md5, + sha256: chart.sha256, + resourceUri: chart.resourceUri, + parentChart: chart.parentChart, + name: chart.name, + comment: chart.comment, + })); + + const charts = await Promise.all( + chartData.map((chart) => chartService.create(chart)), + ); + + res.json(charts.map(chartMapper)); + } catch (error) { + const message = getErrorMessage(error); + console.error(`Failed to create chart ${message}`); + res.status(400).json({ error: 'Failed to create chart' }); + } + }, +]); + app.patch('/chart/admin', [ isAdmin, async (req: RequestContext, res: Response) => { diff --git a/packages/api/src/baseblocks/chart/chart-public-api.ts b/packages/api/src/baseblocks/chart/chart-public-api.ts index bea873f..53b8e54 100644 --- a/packages/api/src/baseblocks/chart/chart-public-api.ts +++ b/packages/api/src/baseblocks/chart/chart-public-api.ts @@ -4,7 +4,7 @@ import { RequestContext } from '../../util/request-context.type'; import { Response } from 'express'; import { getErrorMessage } from '../../util/error-message'; import { getChartsByMd5, getChartsBySha256 } from './chart.service'; -import { flatten, keyBy } from 'lodash-es'; +import { flatten, uniqBy } from 'lodash-es'; const app = createApp(); export const handler = createAuthenticatedHandler(app); @@ -17,22 +17,30 @@ app.post('/chart/public', [ sha256: string[]; }; - /** @TODO Ask herman how to batch query */ const md5Charts = flatten( await Promise.all(md5.map((md5) => getChartsByMd5(md5))), ); - const md5ChartsKeyedByMd5 = keyBy(md5Charts, 'md5'); - - console.log(md5Charts); + const md5ChartsFound = md5Charts.map((chart) => chart.md5); + const md5ChartsNotFound = md5.filter( + (md5) => !md5ChartsFound.includes(md5), + ); const sha256Charts = flatten( await Promise.all(sha256.map((sha256) => getChartsBySha256(sha256))), ); - const sha256ChartsKeyedBySha256 = keyBy(sha256Charts, 'sha256'); + const sha256ChartsFound = sha256Charts.map((chart) => chart.sha256); + const sha256ChartsNotFound = sha256.filter( + (sha256) => !sha256ChartsFound.includes(sha256), + ); res.json({ - md5: md5ChartsKeyedByMd5, - sha256: sha256ChartsKeyedBySha256, + resources: uniqBy([...md5Charts, ...sha256Charts], 'resourceUri').map( + (chart) => chart.resourceUri, + ), + metadata: { + success: [...md5ChartsFound, ...sha256ChartsFound], + failure: [...md5ChartsNotFound, ...sha256ChartsNotFound], + }, }); } catch (error) { const message = getErrorMessage(error);