Initial commit
This commit is contained in:
Executable
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @typedef {Object} AuthorizeOptions
|
||||
* @property {string} [role] - Role which the user should be in
|
||||
* @property {string[]} [roles] - Array of roles which the user should be in
|
||||
*/
|
||||
export class AuthorizeOptions {
|
||||
/** @type {?string} **/
|
||||
role;
|
||||
/** @type {string[]} **/
|
||||
roles;
|
||||
|
||||
constructor(roles) {
|
||||
if(typeof roles === 'string') {
|
||||
this.role = roles;
|
||||
}
|
||||
if (roles instanceof Array) {
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+154
@@ -0,0 +1,154 @@
|
||||
function normalizeIp(ip){
|
||||
// Oddly enough, FF passing in the clientIp is different from chrome?!
|
||||
return ip === "::ffff:127.0.0.1" ? "::1" : ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef File
|
||||
* @property {Buffer} buffer
|
||||
* @property {string} encoding
|
||||
* @property {string} fileName
|
||||
* @property {string} mimetype
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request context
|
||||
*/
|
||||
export class RequestContext{
|
||||
/** @param {RequestContext} c */
|
||||
constructor(c){
|
||||
if(c){
|
||||
this.parameters = c.parameters??{};
|
||||
this.queryParameters = c.queryParameters??{};
|
||||
this.body = c.body;
|
||||
this.session = c.session;
|
||||
this.method = c.method;
|
||||
this.endpoint = c.endpoint;
|
||||
this.clientIp = c.clientIp;
|
||||
this.host = c.host;
|
||||
this.headers = c.headers;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Object<string,string>} */
|
||||
parameters = {};
|
||||
/** @type {Object<string,string>} */
|
||||
queryParameters = {};
|
||||
/** @type {*} */
|
||||
body = undefined;
|
||||
/** @type {AuthSession} */
|
||||
session;
|
||||
/** @type {'GET'|'POST'|'PATCH'|'DELETE'|'PUT'} */
|
||||
method;
|
||||
/** @type {string} */
|
||||
endpoint;
|
||||
/** @type {string} */
|
||||
clientIp;
|
||||
/** @type {string} */
|
||||
host;
|
||||
/** @type {Object} */
|
||||
headers;
|
||||
/** @type {string} */
|
||||
rawBody = undefined;
|
||||
|
||||
get queryString(){
|
||||
let query = Array.from(Object.entries(this.queryParameters));
|
||||
if(query.length>0){
|
||||
return "?" + query.map(
|
||||
([key,value])=>
|
||||
value!==null&&value!==undefined
|
||||
? [encodeURIComponent(key), encodeURIComponent(value)].join('=')
|
||||
: encodeURIComponent(key)
|
||||
).join("&");
|
||||
}else{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class KoaRequestContext extends RequestContext{
|
||||
constructor(koaCtx){
|
||||
super({
|
||||
parameters: koaCtx.params,
|
||||
queryParameters: koaCtx.query,
|
||||
body: koaCtx.request?.body,
|
||||
rawBody: koaCtx.request?.rawBody,
|
||||
session: koaCtx.state?.session,
|
||||
method: koaCtx.method,
|
||||
endpoint: koaCtx.path? koaCtx.path.slice((koaCtx.path.indexOf("/api/")??0) + '/api'.length) : '/',
|
||||
clientIp: normalizeIp((
|
||||
koaCtx.request.headers["X-Forwarded-For".toLowerCase()]
|
||||
?? koaCtx.request.headers["X-Real-IP".toLowerCase()]
|
||||
?? koaCtx.request.headers["X-Orig-IP".toLowerCase()]
|
||||
?? koaCtx.request.ip
|
||||
)?.toString() ?? undefined),
|
||||
host: (koaCtx.request.headers["host"])?.toString() ?? undefined,
|
||||
headers: koaCtx.request.headers
|
||||
});
|
||||
this.#koaCtx = koaCtx;
|
||||
}
|
||||
|
||||
|
||||
// Private
|
||||
#koaCtx;
|
||||
|
||||
set(key, value) {
|
||||
this.#koaCtx.set(key, value);
|
||||
}
|
||||
|
||||
/** @type {request.Request} */
|
||||
get req(){
|
||||
return this.#koaCtx.req;
|
||||
}
|
||||
/** @type {request.Response} */
|
||||
get res(){
|
||||
return this.#koaCtx.res;
|
||||
}
|
||||
/** @type {string} */
|
||||
get path(){
|
||||
return this.#koaCtx.path;
|
||||
}
|
||||
set attachment(fileName){
|
||||
return this.#koaCtx.attachment(fileName);
|
||||
}
|
||||
set type(type){
|
||||
return this.#koaCtx.type = type;
|
||||
}
|
||||
/** @type {File} */
|
||||
get file(){
|
||||
return this.#koaCtx.file;
|
||||
}
|
||||
/** @type {[File]} */
|
||||
get files(){
|
||||
return this.#koaCtx.files;
|
||||
}
|
||||
|
||||
set status(value) {
|
||||
this.#koaCtx.status = value;
|
||||
}
|
||||
/** @type {Instance} */
|
||||
get currentInstance(){
|
||||
return this.#koaCtx.currentInstance;
|
||||
}
|
||||
|
||||
/** @param {Instance} value */
|
||||
set currentInstance(value) {
|
||||
this.#koaCtx.currentInstance = value;
|
||||
}
|
||||
|
||||
/** @type {AuthorizationInfo} */
|
||||
get authorizationInfo() {
|
||||
return this.#koaCtx.authorizationInfo;
|
||||
}
|
||||
set authorizationInfo(authorizationInfo) {
|
||||
this.#koaCtx.authorizationInfo = authorizationInfo;
|
||||
}
|
||||
|
||||
/** @type {UserInfo} */
|
||||
get userInfo() {
|
||||
return this.#koaCtx.userInfo;
|
||||
}
|
||||
set userInfo(userInfo) {
|
||||
this.#koaCtx.userInfo = userInfo;
|
||||
}
|
||||
}
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
/**
|
||||
* @typedef {'GET'|'POST'|'PATCH'|'DELETE'|'PUT'} HttpMethod
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {EndpointOptions}
|
||||
* @property {[function]} middlewares - Additional middlewares
|
||||
* @property {AuthorizeOptions} authorization - Authorization info
|
||||
*/
|
||||
export class EndpointOptions {
|
||||
/** @type {function[]} **/
|
||||
middlewares = [];
|
||||
/** @type {?AuthorizeOptions} **/
|
||||
authorizeOptions;
|
||||
|
||||
/**
|
||||
* @param {function[]} middlewares
|
||||
* @param {?AuthorizeOptions=} [authorizeOptions]
|
||||
*/
|
||||
constructor(middlewares, authorizeOptions= null) {
|
||||
this.middlewares = middlewares;
|
||||
this.authorizeOptions = authorizeOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {HttpEndpoint}
|
||||
* @property {HttpMethod} method
|
||||
* @property {string} route
|
||||
* @property {?EndpointOptions} options
|
||||
*/
|
||||
export class HttpEndpoint {
|
||||
/** @type {HttpMethod} **/
|
||||
method;
|
||||
/** @type {string} **/
|
||||
route;
|
||||
/** @type {?EndpointOptions} **/
|
||||
options;
|
||||
/** @type {function(KoaRequestContext, function)} **/
|
||||
handler;
|
||||
|
||||
/**
|
||||
* @param {HttpMethod} method
|
||||
* @param {string} route
|
||||
* @param {function(KoaRequestContext, function)} handler
|
||||
* @param {?EndpointOptions=} [options]
|
||||
*/
|
||||
constructor(method, route, handler, options = null) {
|
||||
this.method = method;
|
||||
this.route = route;
|
||||
this.handler = handler;
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpGet extends HttpEndpoint {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {function(KoaRequestContext, function)} handler
|
||||
* @param {?EndpointOptions=} [options]
|
||||
*/
|
||||
constructor(route, handler, options = null) {
|
||||
super('GET', route, handler, options);
|
||||
}
|
||||
}
|
||||
export class HttpPost extends HttpEndpoint {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {function(KoaRequestContext, function)} handler
|
||||
* @param {?EndpointOptions=} [options]
|
||||
*/
|
||||
constructor(route, handler, options = null) {
|
||||
super('POST', route, handler, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpPatch extends HttpEndpoint {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {function(KoaRequestContext, function)} handler
|
||||
* @param {?EndpointOptions=} [options]
|
||||
*/
|
||||
constructor(route, handler, options = null) {
|
||||
super('PATCH', route, handler, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpPut extends HttpEndpoint {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {function(KoaRequestContext, function)} handler
|
||||
* @param {?EndpointOptions=} [options]
|
||||
*/
|
||||
constructor(route, handler, options = null) {
|
||||
super('PUT', route, handler, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpDelete extends HttpEndpoint {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {function(KoaRequestContext, function)} handler
|
||||
* @param {?EndpointOptions=} [options]
|
||||
*/
|
||||
constructor(route, handler, options = null) {
|
||||
super('DELETE', route, handler, options);
|
||||
}
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
export * from "./authorize";
|
||||
export * from "./endpoint";
|
||||
export * from "./router.js";
|
||||
export * from "./context";
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
import {KoaRequestContext} from "./context";
|
||||
import {hasRole, hasSite} from "../../middleware/authorization";
|
||||
import Router from "@koa/router";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} path
|
||||
*/
|
||||
export class ApiRouter {
|
||||
/** @type {string} **/
|
||||
path;
|
||||
|
||||
/** @type {HttpEndpoint[]} **/
|
||||
endpoints = [];
|
||||
|
||||
/** @type {?AuthorizeOptions} **/
|
||||
authorizeOptions;
|
||||
|
||||
/** @type {Router} **/
|
||||
koaRouter;
|
||||
|
||||
routerAuthValidator;
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {?AuthorizeOptions=} [authorizeOptions]
|
||||
*/
|
||||
constructor(path, authorizeOptions = null) {
|
||||
this.path = path;
|
||||
|
||||
// Define a matching router
|
||||
this.koaRouter = new Router();
|
||||
|
||||
this.authorizeOptions = authorizeOptions;
|
||||
this.routerAuthValidator = this.generateAuthorizeHandler(authorizeOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HttpEndpoint} endpoint
|
||||
*/
|
||||
registerEndpoint(endpoint) {
|
||||
let method = endpoint.method;
|
||||
let route = endpoint.route;
|
||||
let authValidator = this.generateAuthorizeHandler(endpoint.options?.authorizeOptions);
|
||||
|
||||
let handler = async (ctx, next) => {
|
||||
let reqCtx = new KoaRequestContext(ctx);
|
||||
if(this.routerAuthValidator) {
|
||||
await this.routerAuthValidator(reqCtx);
|
||||
}
|
||||
if(authValidator) {
|
||||
await authValidator(reqCtx);
|
||||
}
|
||||
ctx.body = await Promise.resolve(endpoint.handler(reqCtx, next));
|
||||
};
|
||||
|
||||
let methodMapping = {
|
||||
'GET': 'get',
|
||||
'POST': 'post',
|
||||
'PATCH': 'patch',
|
||||
'PUT': 'put',
|
||||
'DELETE': 'delete'
|
||||
};
|
||||
|
||||
if(endpoint.options?.middlewares) {
|
||||
this.koaRouter[methodMapping[method]](route, ...endpoint.options.middlewares, handler);
|
||||
} else {
|
||||
this.koaRouter[methodMapping[method]](route, handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?AuthorizeOptions} authorizeOptions
|
||||
* @returns {(function(*, *): Promise<void>)|*}
|
||||
*/
|
||||
generateAuthorizeHandler(authorizeOptions) {
|
||||
if (authorizeOptions) {
|
||||
|
||||
return async (/** @param {KoaRequestContext} **/ctx) => {
|
||||
if (authorizeOptions.role || authorizeOptions.roles) {
|
||||
const roles = [
|
||||
authorizeOptions.role,
|
||||
...(authorizeOptions.roles || [])
|
||||
].filter(x => x);
|
||||
await hasRole(roles, ctx);
|
||||
const {parameters: {site}} = ctx;
|
||||
if(site){
|
||||
await hasSite(site, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Router} app
|
||||
*/
|
||||
register(app) {
|
||||
// Register it as middleware in the app (which may in turn have been a KoaRouter)
|
||||
app.use(this.path, this.koaRouter.routes(), this.koaRouter.allowedMethods());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Router from "@koa/router";
|
||||
import {publicAlbumsApi} from "./public/albums";
|
||||
import {s3Api} from "./public/s3/index.js";
|
||||
import {privateInstancesApi} from "./private/instances.js";
|
||||
import {privateAlbumsApi} from "./private/albums.js";
|
||||
|
||||
export const api = new Router();
|
||||
|
||||
publicAlbumsApi.register(api);
|
||||
s3Api.register(api);
|
||||
privateInstancesApi.register(api);
|
||||
privateAlbumsApi.register(api);
|
||||
@@ -0,0 +1,154 @@
|
||||
import {ApiRouter, AuthorizeOptions, HttpDelete, HttpGet, HttpPatch, HttpPost, HttpPut} from "../api-routing/index.js";
|
||||
import {BadRequestError, ValidationError} from "../../models/errors/index.js";
|
||||
import {albums} from "../../services/albums.js";
|
||||
import multer from "@koa/multer";
|
||||
import {instances} from "../../services/instances.js";
|
||||
|
||||
const upload = multer();
|
||||
class AlbumsApi extends ApiRouter {
|
||||
|
||||
|
||||
constructor() {
|
||||
super("/instances/:instanceId", new AuthorizeOptions(["instance-manager", "admin"]));
|
||||
this.registerEndpoint(new HttpGet("/albums", this.fetchAlbums));
|
||||
this.registerEndpoint(new HttpPost("/albums", this.create));
|
||||
this.registerEndpoint(new HttpPut("/albums/:albumId", this.update));
|
||||
this.registerEndpoint(new HttpPatch("/albums/:albumId", this.patch));
|
||||
this.registerEndpoint(new HttpDelete("/albums/:albumId", this.delete));
|
||||
this.registerEndpoint(new HttpPost("/albums/:albumId/move-up", this.moveUp));
|
||||
this.registerEndpoint(new HttpPost("/albums/:albumId/move-down", this.moveDown));
|
||||
this.registerEndpoint(new HttpPost("/albums/:albumId/upload-photos", this.uploadPhotos, {
|
||||
middlewares: [upload.array('photos')]
|
||||
}
|
||||
));
|
||||
this.registerEndpoint(new HttpDelete("/albums/:albumId/photos/:photoId", this.deletePhoto));
|
||||
this.registerEndpoint(new HttpPatch("/albums/:albumId/photos/:photoId", this.patchPhoto));
|
||||
this.registerEndpoint(new HttpPost("/albums/:albumId/photos/:photoId/move-up", this.movePhotoUp));
|
||||
this.registerEndpoint(new HttpPost("/albums/:albumId/photos/:photoId/move-down", this.movePhotoDown));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async fetchAlbums(ctx) {
|
||||
const {parameters: {instanceId}} = ctx;
|
||||
return {
|
||||
data: await albums.getAlbums(instanceId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async create({parameters: {instanceId}, body}) {
|
||||
return {
|
||||
data: await albums.createAlbum(instanceId, body)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async update({parameters: {instanceId, albumId}, body: {name, description, sort}}) {
|
||||
sort = sort || "auto";
|
||||
if (!["manual", "auto"].includes(sort)) {
|
||||
throw new ValidationError([{
|
||||
field: "sort",
|
||||
message: `Parameter 'sort' can only be 'manual' or 'auto', not ${sort}.`
|
||||
}]);
|
||||
}
|
||||
return {
|
||||
data: await albums.updateAlbum({instanceId, albumId, name, description, sort})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async patch({parameters: {instanceId, albumId}, body}) {
|
||||
return {
|
||||
data: await albums.patchAlbum(instanceId, albumId, body)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async delete(ctx) {
|
||||
const {parameters: {instanceId, albumId}} = ctx;
|
||||
await albums.deleteAlbum(instanceId, albumId);
|
||||
return {
|
||||
data: await albums.getAlbums()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async moveUp(ctx) {
|
||||
const {parameters: {instanceId, albumId}} = ctx;
|
||||
return {
|
||||
data: await albums.moveAlbum(instanceId, albumId, "up")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async moveDown(ctx) {
|
||||
const {parameters: {instanceId, albumId}} = ctx;
|
||||
return {
|
||||
data: await albums.moveAlbum(instanceId, albumId, "down")
|
||||
};
|
||||
}
|
||||
|
||||
async uploadPhotos(ctx) {
|
||||
const {parameters: {instanceId, albumId}} = ctx;
|
||||
if (!ctx.files) {
|
||||
throw new BadRequestError("No files were uploaded for field 'photos'");
|
||||
}
|
||||
return {
|
||||
data: await albums.uploadPhotos(instanceId, albumId, ctx.files)
|
||||
};
|
||||
}
|
||||
|
||||
async deletePhoto(ctx) {
|
||||
const {parameters: {instanceId, albumId, photoId}} = ctx;
|
||||
return {
|
||||
data: await albums.deletePhoto(instanceId, albumId, photoId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async patchPhoto({parameters: {instanceId, albumId}, body}) {
|
||||
return {
|
||||
data: await albums.patchPhoto(instanceId, albumId, parameters.photoId, body)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async movePhotoUp(ctx) {
|
||||
const {parameters: {instanceId, albumId, photoId}} = ctx;
|
||||
return {
|
||||
data: await albums.movePhoto(instanceId, albumId, photoId, "up")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async movePhotoDown(ctx) {
|
||||
const {parameters: {instanceId, albumId, photoId}} = ctx;
|
||||
return {
|
||||
data: await albums.movePhoto(instanceId, albumId, photoId, "down")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const instance = new AlbumsApi();
|
||||
export {instance as privateAlbumsApi};
|
||||
@@ -0,0 +1,75 @@
|
||||
import {ApiRouter, AuthorizeOptions, HttpGet, HttpPatch, HttpPost, HttpPut} from "../api-routing/index.js";
|
||||
import {instances} from "../../services/instances.js";
|
||||
import {BadRequestError} from "../../models/errors/index.js";
|
||||
import {albums} from "../../services/albums.js";
|
||||
import multer from "@koa/multer";
|
||||
|
||||
const upload = multer();
|
||||
class InstancesApi extends ApiRouter {
|
||||
constructor() {
|
||||
super("/instances", new AuthorizeOptions(["instance-manager", "admin"]));
|
||||
this.registerEndpoint(new HttpGet("/", this.fetchInstances));
|
||||
this.registerEndpoint(new HttpPost("/", this.createInstance, {
|
||||
authorizeOptions: new AuthorizeOptions("admin")
|
||||
}));
|
||||
this.registerEndpoint(new HttpGet("/:instanceId", this.fetchInstance));
|
||||
this.registerEndpoint(new HttpPut("/:instanceId", this.updateInstance));
|
||||
this.registerEndpoint(new HttpPost("/:instanceId/upload-cover-photo", this.uploadPhoto, {
|
||||
middlewares: [upload.array('cover-photo')]
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async fetchInstances(ctx) {
|
||||
return {
|
||||
data: await instances.getInstances(ctx.userInfo.available_photos_sites)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async fetchInstance(ctx) {
|
||||
const {parameters: {instanceId}} = ctx;
|
||||
return {
|
||||
data: await instances.getInstance(instanceId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async updateInstance({parameters, body}) {
|
||||
return {
|
||||
data: await instances.updateInstance({
|
||||
...body,
|
||||
instanceId: parameters.instanceId
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async createInstance({body}) {
|
||||
return {
|
||||
data: await instances.createInstance(body)
|
||||
};
|
||||
}
|
||||
|
||||
async uploadPhoto(ctx) {
|
||||
const {parameters: {instanceId}} = ctx;
|
||||
if (!ctx.files) {
|
||||
throw new BadRequestError("No file was uploaded for field 'photo'");
|
||||
}
|
||||
return {
|
||||
data: await instances.setCoverPhoto(instanceId, ctx.files[0])
|
||||
};
|
||||
}
|
||||
}
|
||||
// Singleton
|
||||
const instance = new InstancesApi();
|
||||
export {instance as privateInstancesApi};
|
||||
@@ -0,0 +1,25 @@
|
||||
import {ApiRouter, HttpGet} from "../../api-routing";
|
||||
import {albums} from "../../../services/albums.js";
|
||||
|
||||
class PublicAlbumsApi extends ApiRouter {
|
||||
|
||||
constructor() {
|
||||
super("/albums");
|
||||
this.registerEndpoint(new HttpGet("/", this.getAll))
|
||||
}
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async getAll(ctx) {
|
||||
return {
|
||||
data:{
|
||||
albums: await albums.getAlbums(ctx.currentInstance.instanceId),
|
||||
instance: ctx.currentInstance
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const instance = new PublicAlbumsApi();
|
||||
export {instance as publicAlbumsApi};
|
||||
@@ -0,0 +1,61 @@
|
||||
import {ApiRouter, HttpGet} from "../../api-routing";
|
||||
import {minio} from "../../../services/minio.js";
|
||||
import {thumbnailer} from "../../../services/thumbnailer.js";
|
||||
|
||||
class S3Api extends ApiRouter {
|
||||
constructor() {
|
||||
super("/s3");
|
||||
this.registerEndpoint(new HttpGet("/:filePath(.*)", this.getFile))
|
||||
}
|
||||
/**
|
||||
* @param {KoaRequestContext} ctx
|
||||
*/
|
||||
async getFile(ctx) {
|
||||
const {parameters: {filePath}, queryParameters: {width, height, percentage, fit}} = ctx;
|
||||
const stream = await minio.downloadFile(filePath);
|
||||
const headersToCopy = [
|
||||
"content-type",
|
||||
"content-length",
|
||||
"last-modified",
|
||||
"etag"
|
||||
];
|
||||
for(let header in stream.headers) {
|
||||
if(headersToCopy.includes(header)) {
|
||||
ctx.set(header, stream.headers[header]);
|
||||
}
|
||||
}
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setSeconds(expiryDate.getSeconds() + 10);
|
||||
ctx.set("expires", expiryDate.toISOString());
|
||||
ctx.set("content-location", filePath);
|
||||
ctx.set("cache-control", "public, max-age=10");
|
||||
ctx.set("date", stream.headers["last-modified"]);
|
||||
|
||||
for(let header in ctx.headers) {
|
||||
if(header.toLowerCase() === "if-none-match" &&
|
||||
ctx.headers[header] === stream.headers["etag"]) {
|
||||
ctx.status = 304;
|
||||
return null;
|
||||
} else if(header.toLowerCase() === "if-modified-since" &&
|
||||
ctx.headers[header] === stream.headers["last-modified"]) {
|
||||
ctx.status = 304;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if(width || height || percentage) {
|
||||
// return thumbnail
|
||||
const options = {};
|
||||
if(!isNaN(Number(width))) options.width = Number(width);
|
||||
if(!isNaN(Number(height))) options.height = Number(height);
|
||||
if(!isNaN(Number(percentage))) options.percentage = Number(percentage);
|
||||
if(fit) options.fit = fit;
|
||||
|
||||
return await thumbnailer.fromStream(stream, options, fit);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const instance = new S3Api();
|
||||
export {instance as s3Api};
|
||||
Reference in New Issue
Block a user