mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-13 16:22:39 +10:00
object storage interface + utility functions
New ObjectBackend class that requires implementors to specify a few basic functions, and it handles the permission logic on top of that. Hopefully there is enough abstraction to suite further use cases!
This commit is contained in:
@ -19,6 +19,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nuxt": "^3.13.0",
|
"nuxt": "^3.13.0",
|
||||||
"prisma": "^5.20.0",
|
"prisma": "^5.20.0",
|
||||||
|
"stream": "^0.0.3",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
|
|||||||
@ -18,6 +18,9 @@ model User {
|
|||||||
username String @unique
|
username String @unique
|
||||||
admin Boolean @default(false)
|
admin Boolean @default(false)
|
||||||
|
|
||||||
|
email String
|
||||||
|
displayName String
|
||||||
|
|
||||||
authMecs LinkedAuthMec[]
|
authMecs LinkedAuthMec[]
|
||||||
clients Client[]
|
clients Client[]
|
||||||
}
|
}
|
||||||
|
|||||||
18
server/internal/metadata/types.d.ts
vendored
18
server/internal/metadata/types.d.ts
vendored
@ -1,4 +1,5 @@
|
|||||||
import { Developer, Publisher } from "@prisma/client";
|
import { Developer, Publisher } from "@prisma/client";
|
||||||
|
import { ObjectReference } from "../objects";
|
||||||
|
|
||||||
export interface GameMetadataSearchResult {
|
export interface GameMetadataSearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
@ -14,7 +15,6 @@ export interface GameMetadataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type InternalGameMetadataResult = GameMetadataSearchResult & GameMetadataSource;
|
export type InternalGameMetadataResult = GameMetadataSearchResult & GameMetadataSource;
|
||||||
export type RemoteObject = string;
|
|
||||||
|
|
||||||
export interface GameMetadata {
|
export interface GameMetadata {
|
||||||
id: string;
|
id: string;
|
||||||
@ -31,10 +31,10 @@ export interface GameMetadata {
|
|||||||
reviewRating: number;
|
reviewRating: number;
|
||||||
|
|
||||||
// Created with another utility function
|
// Created with another utility function
|
||||||
icon: RemoteObject,
|
icon: ObjectReference,
|
||||||
banner: RemoteObject,
|
banner: ObjectReference,
|
||||||
art: RemoteObject[],
|
art: ObjectReference[],
|
||||||
screenshots: RemoteObject[],
|
screenshots: ObjectReference[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublisherMetadata {
|
export interface PublisherMetadata {
|
||||||
@ -43,8 +43,8 @@ export interface PublisherMetadata {
|
|||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
logo: RemoteObject;
|
logo: ObjectReference;
|
||||||
banner: RemoteObject;
|
banner: ObjectReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeveloperMetadata = PublisherMetadata;
|
export type DeveloperMetadata = PublisherMetadata;
|
||||||
@ -55,12 +55,12 @@ export interface _FetchGameMetadataParams {
|
|||||||
publisher: (query: string) => Promise<Publisher>
|
publisher: (query: string) => Promise<Publisher>
|
||||||
developer: (query: string) => Promise<Developer>
|
developer: (query: string) => Promise<Developer>
|
||||||
|
|
||||||
createObject: (url: string) => RemoteObject
|
createObject: (url: string) => ObjectReference
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface _FetchPublisherMetadataParams {
|
export interface _FetchPublisherMetadataParams {
|
||||||
query: string;
|
query: string;
|
||||||
createObject: (url: string) => RemoteObject;
|
createObject: (url: string) => ObjectReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams;
|
export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams;
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Objects are basically files, like images or downloads, that have a set of metadata and permissions attached
|
||||||
|
* They're served by the API from the /api/v1/object/${objectId} endpoint.
|
||||||
|
*
|
||||||
|
* It supports streams and buffers, depending on the use case. Buffers will likely only be used internally if
|
||||||
|
* the data needs to be manipulated somehow.
|
||||||
|
*
|
||||||
|
* Objects are designed to be created once, and link to a single ID. For example, each user gets a single object
|
||||||
|
* that's tied to their profile picture. If they want to update their profile picture, they overwrite that object.
|
||||||
|
*
|
||||||
|
* Permissions are a list of strings. Each permission string is in the id:permission format. Eg
|
||||||
|
* anonymous:read
|
||||||
|
* myUserId:read
|
||||||
|
* anotherUserId:write
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
export type ObjectReference = string;
|
||||||
|
export type ObjectMetadata = {
|
||||||
|
mime: string;
|
||||||
|
permissions: string[];
|
||||||
|
userMetadata: { [key: string]: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ObjectPermission {
|
||||||
|
Read = "read",
|
||||||
|
Write = "write",
|
||||||
|
Delete = "delete",
|
||||||
|
}
|
||||||
|
export const ObjectPermissionPriority: Array<ObjectPermission> = [
|
||||||
|
ObjectPermission.Read,
|
||||||
|
ObjectPermission.Write,
|
||||||
|
ObjectPermission.Delete,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type Source = Readable | Buffer;
|
||||||
|
|
||||||
|
export type Object = { metadata: ObjectMetadata };
|
||||||
|
export type StreamObject = Object & { stream: Readable };
|
||||||
|
export type BufferObject = Object & { buffer: Buffer };
|
||||||
|
|
||||||
|
export abstract class ObjectBackend {
|
||||||
|
// Interface functions, not designed to be called directly.
|
||||||
|
// They don't check permissions to provide any utilities
|
||||||
|
abstract fetch(id: ObjectReference): Promise<Object | undefined>;
|
||||||
|
abstract write(id: ObjectReference, source: Source): Promise<boolean>;
|
||||||
|
abstract create(
|
||||||
|
source: Source,
|
||||||
|
metadata: ObjectMetadata
|
||||||
|
): Promise<ObjectReference | undefined>;
|
||||||
|
abstract delete(id: ObjectReference): Promise<boolean>;
|
||||||
|
abstract fetchMetadata(
|
||||||
|
id: ObjectReference
|
||||||
|
): Promise<ObjectMetadata | undefined>;
|
||||||
|
abstract writeMetadata(
|
||||||
|
id: ObjectReference,
|
||||||
|
metadata: ObjectMetadata
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
async fetchWithPermissions(id: ObjectReference, userId: string) {
|
||||||
|
const metadata = await this.fetchMetadata(id);
|
||||||
|
if (!metadata) return;
|
||||||
|
|
||||||
|
// We only need one permission, so find instead of filter is faster
|
||||||
|
const myPermissions = metadata.permissions.find((e) => {
|
||||||
|
if (e.startsWith(userId)) return true;
|
||||||
|
if (e.startsWith("anonymous")) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!myPermissions) {
|
||||||
|
// We do not have access to this object
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because any permission can be read or up, we automatically know we can read this object
|
||||||
|
// So just straight return the object
|
||||||
|
return await this.fetch(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to fetch a remote resource, it doesn't make sense
|
||||||
|
// to immediately fetch the object, *then* check permissions.
|
||||||
|
// Instead the caller can pass a simple anonymous funciton, like
|
||||||
|
// () => $fetch('/my-image');
|
||||||
|
// And if we actually have permission to write, it fetches it then.
|
||||||
|
async writeWithPermissions(
|
||||||
|
id: ObjectReference,
|
||||||
|
sourceFetcher: () => Promise<Source>,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
|
const metadata = await this.fetchMetadata(id);
|
||||||
|
if (!metadata) return;
|
||||||
|
|
||||||
|
const myPermissions = metadata.permissions
|
||||||
|
.filter((e) => {
|
||||||
|
if (e.startsWith(userId)) return true;
|
||||||
|
if (e.startsWith("anonymous")) return true;
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
// Strip IDs from permissions
|
||||||
|
.map((e) => e.split(":").at(1))
|
||||||
|
// Map to priority according to array
|
||||||
|
.map((e) => ObjectPermissionPriority.findIndex((c) => c === e));
|
||||||
|
|
||||||
|
const requiredPermissionIndex = 1;
|
||||||
|
const hasPermission =
|
||||||
|
myPermissions.find((e) => e >= requiredPermissionIndex) != undefined;
|
||||||
|
|
||||||
|
if (!hasPermission) return false;
|
||||||
|
|
||||||
|
const source = await sourceFetcher();
|
||||||
|
const result = await this.write(id, source);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWithPermission(id: ObjectReference, userId: string) {
|
||||||
|
const metadata = await this.fetchMetadata(id);
|
||||||
|
if (!metadata) return false;
|
||||||
|
|
||||||
|
const myPermissions = metadata.permissions
|
||||||
|
.filter((e) => {
|
||||||
|
if (e.startsWith(userId)) return true;
|
||||||
|
if (e.startsWith("anonymous")) return true;
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
// Strip IDs from permissions
|
||||||
|
.map((e) => e.split(":").at(1))
|
||||||
|
// Map to priority according to array
|
||||||
|
.map((e) => ObjectPermissionPriority.findIndex((c) => c === e));
|
||||||
|
|
||||||
|
const requiredPermissionIndex = 2;
|
||||||
|
const hasPermission =
|
||||||
|
myPermissions.find((e) => e >= requiredPermissionIndex) != undefined;
|
||||||
|
|
||||||
|
if (!hasPermission) return false;
|
||||||
|
|
||||||
|
const result = await this.delete(id);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
22
yarn.lock
22
yarn.lock
@ -296,6 +296,16 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime "^3.0.0"
|
mime "^3.0.0"
|
||||||
|
|
||||||
|
"@drop/droplet-linux-x64-gnu@0.3.2":
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.3.2.tgz#9be12f1e61df67837bb225ec67cc98f5af8f703b"
|
||||||
|
integrity sha1-m+EvHmHfZ4N7siXsZ8yY9a+PcDs=
|
||||||
|
|
||||||
|
"@drop/droplet-win32-x64-msvc@0.3.2":
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.3.2.tgz#dc5d0fa8334bf211666e99ca365c900d363f7823"
|
||||||
|
integrity sha1-3F0PqDNL8hFmbpnKNlyQDTY/eCM=
|
||||||
|
|
||||||
"@drop/droplet@^0.3.2":
|
"@drop/droplet@^0.3.2":
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.3.2.tgz#f57cf35e50dfd448b7837f9b4712543ff160e769"
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.3.2.tgz#f57cf35e50dfd448b7837f9b4712543ff160e769"
|
||||||
@ -2094,6 +2104,11 @@ compatx@^0.1.8:
|
|||||||
resolved "https://registry.yarnpkg.com/compatx/-/compatx-0.1.8.tgz#af6f61910ade6ce1073c0fdff23c786bcd75c026"
|
resolved "https://registry.yarnpkg.com/compatx/-/compatx-0.1.8.tgz#af6f61910ade6ce1073c0fdff23c786bcd75c026"
|
||||||
integrity sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==
|
integrity sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==
|
||||||
|
|
||||||
|
component-emitter@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-2.0.0.tgz#3a137dfe66fcf2efe3eab7cb7d5f51741b3620c6"
|
||||||
|
integrity sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==
|
||||||
|
|
||||||
compress-commons@^6.0.2:
|
compress-commons@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e"
|
resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e"
|
||||||
@ -4749,6 +4764,13 @@ std-env@^3.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||||
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
||||||
|
|
||||||
|
stream@^0.0.3:
|
||||||
|
version "0.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.3.tgz#3f3934a900a561ce3e2b9ffbd2819cead32699d9"
|
||||||
|
integrity sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==
|
||||||
|
dependencies:
|
||||||
|
component-emitter "^2.0.0"
|
||||||
|
|
||||||
streamx@^2.15.0:
|
streamx@^2.15.0:
|
||||||
version "2.20.1"
|
version "2.20.1"
|
||||||
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c"
|
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c"
|
||||||
|
|||||||
Reference in New Issue
Block a user