mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat: object storage
This commit is contained in:
		
							parent
							
								
									8e1c85fe4f
								
							
						
					
					
						commit
						96a2725c3e
					
				
							
								
								
									
										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/"
 | 
					    "sql:generate": "node ./dist/infra/sql-generator/"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@aws-sdk/client-s3": "^3.515.0",
 | 
				
			||||||
 | 
					    "@aws-sdk/lib-storage": "^3.515.0",
 | 
				
			||||||
    "@babel/runtime": "^7.22.11",
 | 
					    "@babel/runtime": "^7.22.11",
 | 
				
			||||||
    "@immich/cli": "^2.0.7",
 | 
					    "@immich/cli": "^2.0.7",
 | 
				
			||||||
    "@nestjs/bullmq": "^10.0.1",
 | 
					    "@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: '',
 | 
					    externalDomain: '',
 | 
				
			||||||
    loginPageMessage: '',
 | 
					    loginPageMessage: '',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  storage: {
 | 
				
			||||||
 | 
					    kind: 'local',
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      path: process.env.UPLOAD_LOCATION ?? '',
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum FeatureFlag {
 | 
					export enum FeatureFlag {
 | 
				
			||||||
 | 
				
			|||||||
@ -172,6 +172,18 @@ export enum LogLevel {
 | 
				
			|||||||
  FATAL = 'fatal',
 | 
					  FATAL = 'fatal',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StorageOptionsLocal {
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StorageOptionsS3 {
 | 
				
			||||||
 | 
					  bucket: string;
 | 
				
			||||||
 | 
					  region: string;
 | 
				
			||||||
 | 
					  endpoint: string;
 | 
				
			||||||
 | 
					  accessKeyId: string;
 | 
				
			||||||
 | 
					  secretAccessKey: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SystemConfig {
 | 
					export interface SystemConfig {
 | 
				
			||||||
  ffmpeg: {
 | 
					  ffmpeg: {
 | 
				
			||||||
    crf: number;
 | 
					    crf: number;
 | 
				
			||||||
@ -276,4 +288,10 @@ export interface SystemConfig {
 | 
				
			|||||||
    externalDomain: string;
 | 
					    externalDomain: string;
 | 
				
			||||||
    loginPageMessage: string;
 | 
					    loginPageMessage: string;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					  // TODO(uhthomas): Is this definitely the approach we want to take for
 | 
				
			||||||
 | 
					  // configuring storage?
 | 
				
			||||||
 | 
					  storage: {
 | 
				
			||||||
 | 
					    kind: string;
 | 
				
			||||||
 | 
					    options: StorageOptionsLocal | StorageOptionsS3;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user