parent
bb9a1bdfa6
commit
35cd904a63
32 changed files with 1512 additions and 1243 deletions
@ -0,0 +1,44 @@ |
|||||||
|
|
||||||
|
// This interface describes a JSON format in which the "interesting part"
|
||||||
|
// of the entire database for a user can be imported/exported.
|
||||||
|
// Worth noting is that the IDs used in this format only exist for cross-
|
||||||
|
// referencing between objects. They do not correspond to IDs in the actual
|
||||||
|
// database.
|
||||||
|
// Upon import, they might be replaced, and upon export, they might be randomly
|
||||||
|
|
||||||
|
import { AlbumWithRefsWithId, ArtistWithRefsWithId, isAlbumWithRefs, isArtistWithRefs, isTagWithRefs, isTrackBaseWithRefs, isTrackWithRefs, TagWithRefsWithId, TrackWithRefsWithId } from "../types/resources"; |
||||||
|
|
||||||
|
// generated.
|
||||||
|
export interface DBImportExportFormat { |
||||||
|
tracks: TrackWithRefsWithId[], |
||||||
|
albums: AlbumWithRefsWithId[], |
||||||
|
artists: ArtistWithRefsWithId[], |
||||||
|
tags: TagWithRefsWithId[], |
||||||
|
} |
||||||
|
|
||||||
|
// Get a full export of a user's database (GET).
|
||||||
|
export const DBExportEndpoint = "/export"; |
||||||
|
export type DBExportResponse = DBImportExportFormat; |
||||||
|
|
||||||
|
// Fully replace the user's database by an import (POST).
|
||||||
|
export const DBImportEndpoint = "/import"; |
||||||
|
export type DBImportRequest = DBImportExportFormat; |
||||||
|
export type DBImportResponse = void; |
||||||
|
export const checkDBImportRequest: (v: any) => boolean = (v: any) => { |
||||||
|
return 'tracks' in v && |
||||||
|
'albums' in v && |
||||||
|
'artists' in v && |
||||||
|
'tags' in v && |
||||||
|
v.tracks.reduce((prev: boolean, cur: any) => { |
||||||
|
return prev && isTrackWithRefs(cur); |
||||||
|
}, true) && |
||||||
|
v.albums.reduce((prev: boolean, cur: any) => { |
||||||
|
return prev && isAlbumWithRefs(cur); |
||||||
|
}, true) && |
||||||
|
v.artists.reduce((prev: boolean, cur: any) => { |
||||||
|
return prev && isArtistWithRefs(cur); |
||||||
|
}, true) && |
||||||
|
v.tags.reduce((prev: boolean, cur: any) => { |
||||||
|
return prev && isTagWithRefs(cur); |
||||||
|
}, true); |
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
import React, { useReducer, useState } from 'react'; |
||||||
|
import { WindowState } from "../Windows"; |
||||||
|
import { Box, Paper, Typography, TextField, Button } from "@material-ui/core"; |
||||||
|
import SaveIcon from '@material-ui/icons/Save'; |
||||||
|
import { Alert } from '@material-ui/lab'; |
||||||
|
import * as serverApi from '../../../api/api'; |
||||||
|
|
||||||
|
export interface ManageImportExportWindowState extends WindowState { |
||||||
|
dummy: boolean |
||||||
|
} |
||||||
|
export enum ManageImportExportWindowActions { |
||||||
|
SetDummy = "SetDummy", |
||||||
|
} |
||||||
|
export function ManageImportExportWindowReducer(state: ManageImportExportWindowState, action: any) { |
||||||
|
switch (action.type) { |
||||||
|
case ManageImportExportWindowActions.SetDummy: { |
||||||
|
return state; |
||||||
|
} |
||||||
|
default: |
||||||
|
throw new Error("Unimplemented ManageImportExportWindow state update.") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default function ManageImportExportWindow(props: {}) { |
||||||
|
const [state, dispatch] = useReducer(ManageImportExportWindowReducer, { |
||||||
|
dummy: true, |
||||||
|
}); |
||||||
|
|
||||||
|
return <ManageImportExportWindowControlled state={state} dispatch={dispatch} /> |
||||||
|
} |
||||||
|
|
||||||
|
export function ManageImportExportWindowControlled(props: { |
||||||
|
state: ManageImportExportWindowState, |
||||||
|
dispatch: (action: any) => void, |
||||||
|
}) { |
||||||
|
return <> |
||||||
|
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
||||||
|
<Box |
||||||
|
m={1} |
||||||
|
mt={4} |
||||||
|
width="80%" |
||||||
|
> |
||||||
|
<SaveIcon style={{ fontSize: 80 }} /> |
||||||
|
</Box> |
||||||
|
<Box |
||||||
|
m={1} |
||||||
|
mt={4} |
||||||
|
width="80%" |
||||||
|
> |
||||||
|
<Typography variant="h4">Import / Export</Typography> |
||||||
|
<Box mt={2} /> |
||||||
|
<Typography> |
||||||
|
An exported database contains all your artists, albums, tracks and tags.<br /> |
||||||
|
It is represented as a JSON structure. |
||||||
|
</Typography> |
||||||
|
<Box mt={2} /> |
||||||
|
<Alert severity="warning">Upon importing a previously exported database, your database will be completely replaced!</Alert> |
||||||
|
<Box mt={2} /> |
||||||
|
<a href={(process.env.REACT_APP_BACKEND || "") + serverApi.DBExportEndpoint}> |
||||||
|
<Button variant="outlined">Export</Button> |
||||||
|
</a> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
const { createProxyMiddleware } = require('http-proxy-middleware'); |
||||||
|
|
||||||
|
module.exports = function(app) { |
||||||
|
app.use( |
||||||
|
process.env.REACT_APP_BACKEND, |
||||||
|
createProxyMiddleware({ |
||||||
|
target: 'http://localhost:5000', |
||||||
|
changeOrigin: true, |
||||||
|
}) |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,45 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import { exportDB, importDB } from "../db/ImportExport"; |
||||||
|
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from "./types"; |
||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import { DBExportEndpoint } from "../../client/src/api/api"; |
||||||
|
|
||||||
|
export const DBExport: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
try { |
||||||
|
let db = await exportDB(req.user.id, knex); |
||||||
|
res.status(200); |
||||||
|
res.setHeader('Content-disposition', 'attachment; filename= mudbase.json'); |
||||||
|
res.setHeader('Content-type', 'application/json'); |
||||||
|
res.send( JSON.stringify(db, null, 2) ); |
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const DBImport: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkDBImportRequest(req.body)) { |
||||||
|
const e: EndpointError = { |
||||||
|
name: "EndpointError", |
||||||
|
message: 'Invalid DBImport request', |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.DBImportRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Import DB "); |
||||||
|
|
||||||
|
try { |
||||||
|
await importDB(userId, reqObject, knex) |
||||||
|
res.status(200).send(); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const importExportEndpoints: [ string, string, boolean, EndpointHandler ][] = [ |
||||||
|
[ api.DBExportEndpoint, 'get', true, DBExport ], |
||||||
|
[ api.DBImportEndpoint, 'post', true, DBImport ], |
||||||
|
]; |
Loading…
Reference in new issue