mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 06:31:58 +00:00
feat: object storage
This commit is contained in:
parent
8e1c85fe4f
commit
96a2725c3e
7 changed files with 2936 additions and 4 deletions
2799
server/package-lock.json
generated
2799
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -34,6 +34,8 @@
|
|||
"sql:generate": "node ./dist/infra/sql-generator/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.515.0",
|
||||
"@aws-sdk/lib-storage": "^3.515.0",
|
||||
"@babel/runtime": "^7.22.11",
|
||||
"@immich/cli": "^2.0.7",
|
||||
"@nestjs/bullmq": "^10.0.1",
|
||||
|
|
27
server/src/domain/fs/fs.ts
Normal file
27
server/src/domain/fs/fs.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
export interface FS {
|
||||
// create creates an object with the given name.
|
||||
create(name: string): Promise<Writable>;
|
||||
|
||||
// open opens the named object.
|
||||
open(name: string): Promise<Readable>;
|
||||
|
||||
// remove removes the named object.
|
||||
remove(name: string): Promise<void>;
|
||||
}
|
||||
|
||||
// export interface FS {
|
||||
// // create creates an object with the given name.
|
||||
// create(name: string): Promise<WritableFile>;
|
||||
|
||||
// // open opens the object with the given name.
|
||||
// open(name: string): Promise<ReadableFile>;
|
||||
|
||||
// // remove removes the named object.
|
||||
// remove(name: string): Promise<void>;
|
||||
// }
|
||||
|
||||
// export interface File {
|
||||
// createReadableStream(): Promise<Readable>;
|
||||
// }
|
21
server/src/domain/fs/local.ts
Normal file
21
server/src/domain/fs/local.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { constants, open, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
export class LocalFS {
|
||||
constructor(private dir: string) { }
|
||||
|
||||
async create(name: string): Promise<Writable> {
|
||||
const file = await open(join(this.dir, name), constants.O_WRONLY);
|
||||
return file.createWriteStream();
|
||||
}
|
||||
|
||||
async open(name: string): Promise<Readable> {
|
||||
const file = await open(join(this.dir, name), constants.O_RDONLY);
|
||||
return file.createReadStream();
|
||||
}
|
||||
|
||||
async remove(name: string): Promise<void> {
|
||||
await unlink(join(this.dir, name));
|
||||
}
|
||||
}
|
65
server/src/domain/fs/s3.ts
Normal file
65
server/src/domain/fs/s3.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { PassThrough, Readable, Writable } from "node:stream";
|
||||
import { S3 } from "@aws-sdk/client-s3";
|
||||
import { FS } from "./fs";
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
|
||||
export class S3FS implements FS {
|
||||
s3: S3;
|
||||
|
||||
constructor(private bucket: string) {
|
||||
this.s3 = new S3();
|
||||
}
|
||||
|
||||
async create(name: string): Promise<Writable> {
|
||||
const stream = new PassThrough();
|
||||
const upload = new Upload({
|
||||
client: this.s3,
|
||||
params: {
|
||||
Body: stream,
|
||||
Bucket: this.bucket,
|
||||
Key: name,
|
||||
},
|
||||
});
|
||||
|
||||
// Abort the upload if the stream has finished. Should be a
|
||||
// no-op if the upload has already finished.
|
||||
stream.on('close', () => upload.abort());
|
||||
|
||||
// Close the stream when the upload is finished, or if it
|
||||
// failed.
|
||||
//
|
||||
// TODO: Find a way to bubble up this error.
|
||||
upload.done().then(() => void stream.end(), error => {
|
||||
console.log(`s3 upload failed: ${error}`);
|
||||
stream.end();
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
async open(name: string): Promise<Readable> {
|
||||
const obj = await this.s3.getObject({
|
||||
Bucket: this.bucket,
|
||||
Key: name,
|
||||
});
|
||||
return obj.Body as Readable;
|
||||
// const stream = obj.Body?.transformToWebStream();
|
||||
// if (!stream) {
|
||||
// throw new Error("no body");
|
||||
// }
|
||||
// return Readable.fromWeb(new ReadableStream(stream));
|
||||
}
|
||||
|
||||
async remove(name: string): Promise<void> {
|
||||
await this.s3.deleteObject({
|
||||
Bucket: this.bucket,
|
||||
Key: name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// class ObjectReadable extends Readable {
|
||||
// constructor(private s3: S3, private bucket: string) { }
|
||||
|
||||
|
||||
// }
|
|
@ -140,6 +140,12 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
},
|
||||
storage: {
|
||||
kind: 'local',
|
||||
options: {
|
||||
path: process.env.UPLOAD_LOCATION ?? '',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export enum FeatureFlag {
|
||||
|
@ -168,7 +174,7 @@ export class SystemConfigCore {
|
|||
|
||||
public config$ = new Subject<SystemConfig>();
|
||||
|
||||
private constructor(private repository: ISystemConfigRepository) {}
|
||||
private constructor(private repository: ISystemConfigRepository) { }
|
||||
|
||||
static create(repository: ISystemConfigRepository) {
|
||||
if (!instance) {
|
||||
|
|
|
@ -172,6 +172,18 @@ export enum LogLevel {
|
|||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export interface StorageOptionsLocal {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface StorageOptionsS3 {
|
||||
bucket: string;
|
||||
region: string;
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
|
@ -276,4 +288,10 @@ export interface SystemConfig {
|
|||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
};
|
||||
// TODO(uhthomas): Is this definitely the approach we want to take for
|
||||
// configuring storage?
|
||||
storage: {
|
||||
kind: string;
|
||||
options: StorageOptionsLocal | StorageOptionsS3;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue