1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

WIP refactor container and queuing system (#206)

* refactor microservices to machine-learning

* Update tGithub issue template with correct task syntax

* Added microservices container

* Communicate between service based on queue system

* added dependency

* Fixed problem with having to import BullQueue into the individual service

* Added todo

* refactor server into monorepo with microservices

* refactor database and entity to library

* added simple migration

* Move migrations and database config to library

* Migration works in library

* Cosmetic change in logging message

* added user dto

* Fixed issue with testing not able to find the shared library

* Clean up library mapping path

* Added webp generator to microservices

* Update Github Action build latest

* Fixed issue NPM cannot install due to conflict witl Bull Queue

* format project with prettier

* Modified docker-compose file

* Add GH Action for Staging build:

* Fixed GH action job name

* Modified GH Action to only build & push latest when pushing to main

* Added Test 2e2 Github Action

* Added Test 2e2 Github Action

* Implemented microservice to extract exif

* Added cronjob to scan and generate webp thumbnail  at midnight

* Refactor to ireduce hit time to database when running microservices

* Added error handling to asset services that handle read file from disk

* Added video transcoding queue to process one video at a time

* Fixed loading spinner on web while loading covering the info panel

* Add mechanism to show new release announcement to web and mobile app (#209)

* Added changelog page

* Fixed issues based on PR comments

* Fixed issue with video transcoding run on the server

* Change entry point content for backward combatibility when starting up server

* Added announcement box

* Added error handling to failed silently when the app version checking is not able to make the request to GITHUB

* Added new version announcement overlay

* Update message

* Added messages

* Added logic to check and show announcement

* Add method to handle saving new version

* Added button to dimiss the acknowledge message

* Up version for deployment to the app store
This commit is contained in:
Alex 2022-06-11 16:12:06 -05:00 committed by GitHub
parent 397f8c70b4
commit a8220172f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
192 changed files with 1823 additions and 2117 deletions

View file

@ -16,10 +16,10 @@ Note: Please search to see if an issue already exists for the bug you encountere
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**Task List** **Task List**
[ ] I have read thoroughly the README setup and installation instructions. - [ ] I have read thoroughly the README setup and installation instructions.
[ ] If my setup is different, I have included my docker-compose file. - [ ] If my setup is different, I have included my docker-compose file.
[ ] I have included my redacted `.env` file. - [ ] I have included my redacted `.env` file.
[ ] I have included information on my machine, and environment. - [ ] I have included information on my machine, and environment.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:

View file

@ -4,17 +4,16 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [main] branches: [main]
pull_request:
branches: [main]
jobs: jobs:
build_and_push_server_latest: # This image include both the server and microservices - the two containers can be slitted into separated
# service with its coressponding entry file.
build_and_push_server_monorepo_latest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
# ref: "main" # branch
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
@ -27,23 +26,22 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.0.0
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }} push: true
tags: | tags: |
altran1502/immich-server:latest altran1502/immich-server:latest
build_and_push_microservice_latest: build_and_push_machine_learning_latest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
# ref: "main" # branch
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
@ -56,15 +54,15 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Microservices - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.0.0
with: with:
context: ./microservices context: ./machine-learning
file: ./microservices/Dockerfile file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64 platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }} push: true
tags: | tags: |
altran1502/immich-microservices:latest altran1502/immich-machine-learning:latest
build_and_push_web_latest: build_and_push_web_latest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -72,7 +70,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
# ref: "main" # branch
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0 uses: docker/setup-qemu-action@v2.0.0
@ -91,6 +88,6 @@ jobs:
file: ./web/Dockerfile file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod target: prod
push: ${{ github.event_name != 'pull_request' }} push: true
tags: | tags: |
altran1502/immich-web:latest altran1502/immich-web:latest

View file

@ -0,0 +1,95 @@
name: Build and Push Docker Image - Staging
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# This image include both the server and microservices - the two containers can be slitted into separated
# service with its coressponding entry file.
build_and_push_server_monorepo_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-server:staging
build_and_push_machine_learning_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-machine-learning:staging
build_and_push_web_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.0.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-web:staging

View file

@ -6,7 +6,7 @@ services:
build: build:
context: ../server context: ../server
dockerfile: Dockerfile dockerfile: Dockerfile
command: npm run start:dev command: npm run start:dev immich
expose: expose:
- "3000" - "3000"
volumes: volumes:
@ -23,16 +23,35 @@ services:
networks: networks:
- immich-network - immich-network
immich-microservices: immich-machine-learning:
image: immich-microservices-dev:1.9.0 image: immich-machine-learning-dev:1.9.0
build: build:
context: ../microservices context: ../machine-learning
dockerfile: Dockerfile dockerfile: Dockerfile
command: npm run start:dev command: npm run start:dev
expose: expose:
- "3001" - "3001"
volumes: volumes:
- ../microservices:/usr/src/app - ../machine-learning:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
networks:
- immich-network
immich-microservices:
image: immich-microservices:1.9.0
build:
context: ../server
dockerfile: Dockerfile
command: npm run start:dev microservices
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules - /usr/src/app/node_modules
env_file: env_file:

View file

@ -2,11 +2,8 @@ version: "3.8"
services: services:
immich-server: immich-server:
image: immich-server-staging:latest image: altran1502/immich-server:staging
build: entrypoint: ["/bin/sh", "./start-server.sh"]
context: ../server
dockerfile: Dockerfile
entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose: expose:
- "3000" - "3000"
volumes: volumes:
@ -23,10 +20,23 @@ services:
restart: always restart: always
immich-microservices: immich-microservices:
image: immich-microservices-staging:latest image: altran1502/immich-server:staging
build: entrypoint: ["/bin/sh", "./start-microservices.sh"]
context: ../microservices volumes:
dockerfile: Dockerfile - ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
networks:
- immich-network
restart: always
immich-machine-learning:
image: altran1502/immich-machine-learning:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"] entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose: expose:
- "3001" - "3001"
@ -43,12 +53,8 @@ services:
restart: always restart: always
immich-web: immich-web:
image: immich-web-staging:latest image: altran1502/immich-web:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"] entrypoint: ["/bin/sh", "./entrypoint.sh"]
build:
context: ../web
dockerfile: Dockerfile
target: prod
env_file: env_file:
- .env - .env
ports: ports:
@ -57,14 +63,12 @@ services:
- immich-network - immich-network
restart: always restart: always
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2 image: redis:6.2
networks: networks:
- immich-network - immich-network
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
@ -82,6 +86,7 @@ services:
- 5432:5432 - 5432:5432
networks: networks:
- immich-network - immich-network
restart: always
nginx: nginx:
container_name: proxy_nginx container_name: proxy_nginx
@ -102,4 +107,4 @@ services:
networks: networks:
immich-network: immich-network:
volumes: volumes:
pgdata: pgdata:

View file

@ -3,7 +3,7 @@ version: "3.8"
services: services:
immich-server: immich-server:
image: altran1502/immich-server:latest image: altran1502/immich-server:latest
entrypoint: ["/bin/sh", "./entrypoint.sh"] entrypoint: ["/bin/sh", "./start-server.sh"]
expose: expose:
- "3000" - "3000"
volumes: volumes:
@ -20,7 +20,23 @@ services:
restart: always restart: always
immich-microservices: immich-microservices:
image: altran1502/immich-microservices:latest image: altran1502/immich-server:latest
entrypoint: ["/bin/sh", "./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
networks:
- immich-network
restart: always
immich-machine-learning:
image: altran1502/immich-machine-learning:latest
entrypoint: ["/bin/sh", "./entrypoint.sh"] entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose: expose:
- "3001" - "3001"
@ -47,14 +63,12 @@ services:
- immich-network - immich-network
restart: always restart: always
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2 image: redis:6.2
networks: networks:
- immich-network - immich-network
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
@ -73,7 +87,7 @@ services:
networks: networks:
- immich-network - immich-network
restart: always restart: always
nginx: nginx:
container_name: proxy_nginx container_name: proxy_nginx
image: nginx:latest image: nginx:latest
@ -93,4 +107,4 @@ services:
networks: networks:
immich-network: immich-network:
volumes: volumes:
pgdata: pgdata:

View file

@ -32,4 +32,6 @@ lerna-debug.log*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
upload/

View file

@ -7,7 +7,7 @@ export class ImageClassifierController {
private readonly imageClassifierService: ImageClassifierService, private readonly imageClassifierService: ImageClassifierService,
) { } ) { }
@Post('/tagImage') @Post('/tag-image')
async tagImage(@Body('thumbnailPath') thumbnailPath: string) { async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
return await this.imageClassifierService.tagImage(thumbnailPath); return await this.imageClassifierService.tagImage(thumbnailPath);
} }

View file

@ -8,14 +8,14 @@ async function bootstrap() {
await app.listen(3001, () => { await app.listen(3001, () => {
if (process.env.NODE_ENV == 'development') { if (process.env.NODE_ENV == 'development') {
Logger.log( Logger.log(
'Running Immich Microservices in DEVELOPMENT environment', 'Running Immich Machine Learning in DEVELOPMENT environment',
'IMMICH MICROSERVICES', 'IMMICH MICROSERVICES',
); );
} }
if (process.env.NODE_ENV == 'production') { if (process.env.NODE_ENV == 'production') {
Logger.log( Logger.log(
'Running Immich Microservices in PRODUCTION environment', 'Running Immich Machine Learning in PRODUCTION environment',
'IMMICH MICROSERVICES', 'IMMICH MICROSERVICES',
); );
} }

View file

@ -8,7 +8,7 @@ export class ObjectDetectionController {
private readonly objectDetectionService: ObjectDetectionService, private readonly objectDetectionService: ObjectDetectionService,
) { } ) { }
@Post('/detectObject') @Post('/detect-object')
async detectObject(@Body('thumbnailPath') thumbnailPath: string) { async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
return await this.objectDetectionService.detectObject(thumbnailPath); return await this.objectDetectionService.detectObject(thumbnailPath);
} }

View file

@ -1 +0,0 @@
devenv/

View file

@ -1,3 +0,0 @@
__pycache__/
devenv/
app/upload

View file

@ -1,25 +0,0 @@
## GPU Build
# FROM tensorflow/tensorflow:latest-gpu as gpu
# WORKDIR /code
# COPY ./requirements.txt /code/requirements.txt
# RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# COPY ./app /code/app
## CPU BUILD
FROM python:3.8 as cpu
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

View file

@ -1,37 +0,0 @@
from tensorflow.keras.applications import InceptionV3
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image
import numpy as np
from PIL import Image
import cv2
IMG_SIZE = 299
PREDICTION_MODEL = InceptionV3(weights='imagenet')
def classify_image(image_path: str):
img_path = f'./app/{image_path}'
# img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
target_image = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
resized_target_image = cv2.resize(target_image, (IMG_SIZE, IMG_SIZE))
x = image.img_to_array(resized_target_image)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
preds = PREDICTION_MODEL.predict(x)
result = decode_predictions(preds, top=3)[0]
payload = []
for _, value, _ in result:
payload.append(value)
return payload
def warm_up():
img_path = f'./app/test.png'
img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
PREDICTION_MODEL.predict(x)

File diff suppressed because it is too large Load diff

View file

@ -1,46 +0,0 @@
from pydantic import BaseModel
from fastapi import FastAPI
from .object_detection import object_detection
from .image_classifier import image_classifier
from tf2_yolov4.anchors import YOLOV4_ANCHORS
from tf2_yolov4.model import YOLOv4
HEIGHT, WIDTH = (640, 960)
# Warm up model
image_classifier.warm_up()
app = FastAPI()
class TagImagePayload(BaseModel):
thumbnail_path: str
@app.post("/tagImage")
async def post_root(payload: TagImagePayload):
image_path = payload.thumbnail_path
if image_path[0] == '.':
image_path = image_path[2:]
return image_classifier.classify_image(image_path=image_path)
@app.get("/")
async def test():
object_detection.run_detection()
# image = tf.io.read_file("./app/cars.jpg")
# image = tf.image.decode_image(image)
# image = tf.image.resize(image, (HEIGHT, WIDTH))
# images = tf.expand_dims(image, axis=0) / 255.0
# model = YOLOv4(
# (HEIGHT, WIDTH, 3),
# 80,
# YOLOV4_ANCHORS,
# "darknet",
# )

View file

@ -1,4 +0,0 @@
def run_detection():
print("run detection")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

View file

@ -1,8 +0,0 @@
opencv-python==4.5.5.64
fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0
tensorflow==2.8.0
numpy==1.22.2
pillow==9.0.1
tf2_yolov4==0.1.0

View file

@ -23,4 +23,11 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View file

@ -0,0 +1 @@
* Added announcement pop-up when a new released is pushed out in Github.

View file

@ -58,7 +58,7 @@
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@ -76,5 +76,11 @@
<false /> <false />
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true /> <true />
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.10.1" version_number: "1.11.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View file

@ -13,3 +13,7 @@ const String savedLoginInfoKey = "immichSavedLoginInfoKey";
// Backup Info // Backup Info
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox"; const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
const String backupInfoKey = "immichBackupAlbumInfoKey"; const String backupInfoKey = "immichBackupAlbumInfoKey";
// Github Release Info
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox";
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey";

View file

@ -5,14 +5,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {
@ -24,6 +27,7 @@ void main() async {
await Hive.openBox(userInfoBox); await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
@ -48,10 +52,18 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed"); debugPrint("[APP STATE] resumed");
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed; ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
ref.watch(backupProvider.notifier).resumeBackup();
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
if (isAuthenticated) {
ref.watch(backupProvider.notifier).resumeBackup();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion();
}
ref.watch(websocketProvider.notifier).connect(); ref.watch(websocketProvider.notifier).connect();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion(); ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
break; break;
@ -95,6 +107,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: Stack( home: Stack(
@ -121,6 +135,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]), routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
), ),
const ImmichLoadingOverlay(), const ImmichLoadingOverlay(),
const VersionAnnouncementOverlay(),
], ],
), ),
); );

View file

@ -0,0 +1,57 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
class ReleaseInfoNotifier extends StateNotifier<String> {
ReleaseInfoNotifier() : super("");
void checkGithubReleaseInfo() async {
var dio = Dio();
var box = Hive.box(hiveGithubReleaseInfoBox);
try {
String? localReleaseVersion = box.get(githubReleaseInfoKey);
Response res = await dio.get(
"https://api.github.com/repos/alextran1502/immich/releases/latest",
options: Options(
headers: {"Accept": "application/vnd.github.v3+json"},
),
);
if (res.statusCode == 200) {
String latestTagVersion = res.data["tag_name"];
state = latestTagVersion;
debugPrint("Local release version $localReleaseVersion");
debugPrint("Remote release veresion $latestTagVersion");
if (localReleaseVersion == null && latestTagVersion.isNotEmpty) {
VersionAnnouncementOverlayController.appLoader.show();
return;
}
if (latestTagVersion.isNotEmpty && localReleaseVersion != latestTagVersion) {
VersionAnnouncementOverlayController.appLoader.show();
return;
}
}
} catch (e) {
debugPrint("Error gettting latest release version");
state = "";
}
}
void acknowledgeNewVersion() {
var box = Hive.box(hiveGithubReleaseInfoBox);
box.put(githubReleaseInfoKey, state);
VersionAnnouncementOverlayController.appLoader.hide();
}
}
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>((ref) => ReleaseInfoNotifier());

View file

@ -19,11 +19,6 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
final ServerInfoService _serverInfoService = ServerInfoService(); final ServerInfoService _serverInfoService = ServerInfoService();
getMapboxInfo() async {
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
state = state.copyWith(mapboxInfo: mapboxInfoRes);
}
getServerVersion() async { getServerVersion() async {
ServerVersion? serverVersion = await _serverInfoService.getServerVersion(); ServerVersion? serverVersion = await _serverInfoService.getServerVersion();

View file

@ -1,4 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/mapbox_info.model.dart'; import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
import 'package:immich_mobile/shared/models/server_version.model.dart'; import 'package:immich_mobile/shared/models/server_version.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/services/network.service.dart';
@ -13,15 +14,16 @@ class ServerInfoService {
return ServerInfo.fromJson(response.toString()); return ServerInfo.fromJson(response.toString());
} }
Future<MapboxInfo> getMapboxInfo() async {
Response response = await _networkService.getRequest(url: 'server-info/mapbox');
return MapboxInfo.fromJson(response.toString());
}
Future<ServerVersion?> getServerVersion() async { Future<ServerVersion?> getServerVersion() async {
Response response = await _networkService.getRequest(url: 'server-info/version'); try {
Response response =
await _networkService.getRequest(url: 'server-info/version');
return ServerVersion.fromJson(response.toString()); return ServerVersion.fromJson(response.toString());
} catch (e) {
debugPrint("Error getting server info");
}
return null;
} }
} }

View file

@ -0,0 +1,133 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionAnnouncementOverlay extends HookConsumerWidget {
const VersionAnnouncementOverlay({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
void goToReleaseNote() async {
final Uri _url = Uri.parse('https://github.com/alextran1502/immich/releases/latest');
await launchUrl(_url);
}
void onAcknowledgeTapped() {
ref.watch(releaseInfoProvider.notifier).acknowledgeNewVersion();
}
return ValueListenableBuilder<bool>(
valueListenable: VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier,
builder: (context, shouldShow, child) {
if (shouldShow) {
return Scaffold(
backgroundColor: Colors.black38,
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 307),
child: Wrap(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"New Server Version Available 🎉",
style: TextStyle(
fontSize: 16,
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 14, fontFamily: 'WorkSans', color: Colors.black87, height: 1.2),
children: <TextSpan>[
const TextSpan(
text: 'Hi friend, there is a new release of',
),
const TextSpan(
text: ' Immich ',
style: TextStyle(
fontFamily: "SnowBurstOne",
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
),
const TextSpan(
text: "please take your time to visit the ",
),
TextSpan(
text: "release note",
style: const TextStyle(
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = goToReleaseNote,
),
const TextSpan(
text:
" and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
)
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
visualDensity: VisualDensity.standard,
primary: Colors.indigo,
onPrimary: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
onPressed: onAcknowledgeTapped,
child: const Text(
"Acknowledge",
style: TextStyle(
fontSize: 14,
),
)),
)
],
),
),
),
],
),
),
),
);
} else {
return Container();
}
},
);
}
}
class VersionAnnouncementOverlayController {
static final VersionAnnouncementOverlayController appLoader = VersionAnnouncementOverlayController();
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
void show() {
loaderShowingNotifier.value = true;
}
void hide() {
loaderShowingNotifier.value = false;
}
}

View file

@ -1015,6 +1015,62 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.4"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.3"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:

View file

@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.10.1+16 version: 1.11.0+17
environment: environment:
sdk: ">=2.15.1 <3.0.0" sdk: ">=2.15.1 <3.0.0"
@ -39,6 +39,7 @@ dependencies:
flutter_swipe_detector: ^2.0.0 flutter_swipe_detector: ^2.0.0
equatable: ^2.0.3 equatable: ^2.0.3
image_picker: ^0.8.5+3 image_picker: ^0.8.5+3
url_launcher: ^6.1.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -1,4 +1,4 @@
FROM node:16-alpine3.14 FROM node:16-alpine3.14 as core
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive

View file

@ -23,7 +23,7 @@ import { assetUploadOption } from '../../config/asset-upload.config';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { ServeFileDto } from './dto/serve-file.dto'; import { ServeFileDto } from './dto/serve-file.dto';
import { AssetEntity } from './entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { Response as Res } from 'express'; import { Response as Res } from 'express';
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
@ -31,6 +31,8 @@ import { BackgroundTaskService } from '../../modules/background-task/background-
import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto';
import { CommunicationGateway } from '../communication/communication.gateway'; import { CommunicationGateway } from '../communication/communication.gateway';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('asset') @Controller('asset')
@ -39,7 +41,10 @@ export class AssetController {
private wsCommunicateionGateway: CommunicationGateway, private wsCommunicateionGateway: CommunicationGateway,
private assetService: AssetService, private assetService: AssetService,
private backgroundTaskService: BackgroundTaskService, private backgroundTaskService: BackgroundTaskService,
) { }
@InjectQueue('asset-uploaded-queue')
private assetUploadedQueue: Queue,
) {}
@Post('upload') @Post('upload')
@UseInterceptors( @UseInterceptors(
@ -61,12 +66,23 @@ export class AssetController {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype); const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (uploadFiles.thumbnailData != null && savedAsset) { if (uploadFiles.thumbnailData != null && savedAsset) {
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path); const assetWithThumbnail = await this.assetService.updateThumbnailInfo(
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset); savedAsset,
await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset); uploadFiles.thumbnailData[0].path,
} );
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size); await this.assetUploadedQueue.add(
'asset-uploaded',
{ asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true },
{ jobId: savedAsset.id },
);
} else {
await this.assetUploadedQueue.add(
'asset-uploaded',
{ asset: savedAsset, fileName: file.originalname, fileSize: file.size, hasThumbnail: false },
{ jobId: savedAsset.id },
);
}
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset)); this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
} catch (e) { } catch (e) {

View file

@ -2,9 +2,7 @@ import { Module } from '@nestjs/common';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
import { AssetController } from './asset.controller'; import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from './entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
@ -13,29 +11,19 @@ import { CommunicationModule } from '../communication/communication.module';
@Module({ @Module({
imports: [ imports: [
CommunicationModule, CommunicationModule,
BullModule.registerQueue({
name: 'optimize',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
BullModule.registerQueue({
name: 'background-task',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
TypeOrmModule.forFeature([AssetEntity]),
ImageOptimizeModule,
BackgroundTaskModule, BackgroundTaskModule,
TypeOrmModule.forFeature([AssetEntity]),
BullModule.registerQueue({
name: 'asset-uploaded-queue',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
], ],
controllers: [AssetController], controllers: [AssetController],
providers: [AssetService, AssetOptimizeService, BackgroundTaskService], providers: [AssetService, BackgroundTaskService],
exports: [], exports: [],
}) })
export class AssetModule { } export class AssetModule {}

View file

@ -1,9 +1,9 @@
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common'; import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetEntity, AssetType } from './entities/asset.entity'; import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import _ from 'lodash'; import _ from 'lodash';
import { createReadStream, stat } from 'fs'; import { createReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto'; import { ServeFileDto } from './dto/serve-file.dto';
@ -11,7 +11,6 @@ import { Response as Res } from 'express';
import { promisify } from 'util'; import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto';
import ffmpeg from 'fluent-ffmpeg';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@ -20,12 +19,18 @@ export class AssetService {
constructor( constructor(
@InjectRepository(AssetEntity) @InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>, private assetRepository: Repository<AssetEntity>,
) { } ) {}
public async updateThumbnailInfo(assetId: string, path: string) { public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
return await this.assetRepository.update(assetId, { const updatedAsset = await this.assetRepository
resizePath: path, .createQueryBuilder('assets')
}); .update<AssetEntity>(AssetEntity, { ...asset, resizePath: thumbnailPath })
.where('assets.id = :id', { id: asset.id })
.returning('*')
.updateEntity(true)
.execute();
return updatedAsset.raw[0];
} }
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) { public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
@ -66,13 +71,13 @@ export class AssetService {
try { try {
return await this.assetRepository.find({ return await this.assetRepository.find({
where: { where: {
userId: authUser.id userId: authUser.id,
}, },
relations: ['exifInfo'], relations: ['exifInfo'],
order: { order: {
createdAt: 'DESC' createdAt: 'DESC',
} },
}) });
} catch (e) { } catch (e) {
Logger.error(e, 'getAllAssets'); Logger.error(e, 'getAllAssets');
} }
@ -101,35 +106,45 @@ export class AssetService {
} }
public async downloadFile(query: ServeFileDto, res: Res) { public async downloadFile(query: ServeFileDto, res: Res) {
let file = null; try {
const asset = await this.findOne(query.did, query.aid); let file = null;
const asset = await this.findOne(query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) { if (query.isThumb === 'false' || !query.isThumb) {
const { size } = await fileInfo(asset.originalPath); const { size } = await fileInfo(asset.originalPath);
res.set({ res.set({
'Content-Type': asset.mimeType, 'Content-Type': asset.mimeType,
'Content-Length': size, 'Content-Length': size,
}); });
file = createReadStream(asset.originalPath); file = createReadStream(asset.originalPath);
} else { } else {
const { size } = await fileInfo(asset.resizePath); const { size } = await fileInfo(asset.resizePath);
res.set({ res.set({
'Content-Type': 'image/jpeg', 'Content-Type': 'image/jpeg',
'Content-Length': size, 'Content-Length': size,
}); });
file = createReadStream(asset.resizePath); file = createReadStream(asset.resizePath);
}
return new StreamableFile(file);
} catch (e) {
Logger.error('Error download asset ', e);
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
} }
return new StreamableFile(file);
} }
public async getAssetThumbnail(assetId: string) { public async getAssetThumbnail(assetId: string) {
const asset = await this.assetRepository.findOne({ id: assetId }); try {
const asset = await this.assetRepository.findOne({ id: assetId });
if (asset.webpPath != '') { if (asset.webpPath && asset.webpPath.length > 0) {
return new StreamableFile(createReadStream(asset.webpPath)); return new StreamableFile(createReadStream(asset.webpPath));
} else { } else {
return new StreamableFile(createReadStream(asset.resizePath)); return new StreamableFile(createReadStream(asset.resizePath));
}
} catch (e) {
Logger.error('Error serving asset thumbnail ', e);
throw new InternalServerErrorException('Failed to serve asset thumbnail', 'GetAssetThumbnail');
} }
} }
@ -141,7 +156,6 @@ export class AssetService {
throw new BadRequestException('Asset does not exist'); throw new BadRequestException('Asset does not exist');
} }
// Handle Sending Images // Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
/** /**
@ -154,97 +168,102 @@ export class AssetService {
return new StreamableFile(createReadStream(asset.resizePath)); return new StreamableFile(createReadStream(asset.resizePath));
} }
try {
/** /**
* Serve thumbnail image for both web and mobile app * Serve thumbnail image for both web and mobile app
*/ */
if (query.isThumb === 'false' || !query.isThumb) { if (query.isThumb === 'false' || !query.isThumb) {
res.set({
'Content-Type': asset.mimeType,
});
file = createReadStream(asset.originalPath);
} else {
if (asset.webpPath != '') {
res.set({ res.set({
'Content-Type': 'image/webp', 'Content-Type': asset.mimeType,
}); });
file = createReadStream(asset.webpPath); file = createReadStream(asset.originalPath);
} else {
if (asset.webpPath && asset.webpPath.length > 0) {
res.set({
'Content-Type': 'image/webp',
});
file = createReadStream(asset.webpPath);
} else {
res.set({
'Content-Type': 'image/jpeg',
});
file = createReadStream(asset.resizePath);
}
}
file.on('error', (error) => {
Logger.log(`Cannot create read stream ${error}`);
return new BadRequestException('Cannot Create Read Stream');
});
return new StreamableFile(file);
} catch (e) {
Logger.error('Error serving IMAGE asset ', e);
throw new InternalServerErrorException(`Failed to serve image asset ${e}`, 'ServeFile');
}
} else if (asset.type == AssetType.VIDEO) {
try {
// Handle Video
let videoPath = asset.originalPath;
let mimeType = asset.mimeType;
if (query.isWeb && asset.mimeType == 'video/quicktime') {
videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath;
mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4';
}
const { size } = await fileInfo(videoPath);
const range = headers.range;
if (range) {
/** Extracting Start and End value from Range Header */
let [start, end] = range.replace(/bytes=/, '').split('-');
start = parseInt(start, 10);
end = end ? parseInt(end, 10) : size - 1;
if (!isNaN(start) && isNaN(end)) {
start = start;
end = size - 1;
}
if (isNaN(start) && !isNaN(end)) {
start = size - end;
end = size - 1;
}
// Handle unavailable range request
if (start >= size || end >= size) {
console.error('Bad Request');
// Return the 416 Range Not Satisfiable.
res.status(416).set({
'Content-Range': `bytes */${size}`,
});
throw new BadRequestException('Bad Request Range');
}
/** Sending Partial Content With HTTP Code 206 */
res.status(206).set({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': mimeType,
});
const videoStream = createReadStream(videoPath, { start: start, end: end });
return new StreamableFile(videoStream);
} else { } else {
res.set({ res.set({
'Content-Type': 'image/jpeg', 'Content-Type': mimeType,
});
file = createReadStream(asset.resizePath);
}
}
file.on('error', (error) => {
Logger.log(`Cannot create read stream ${error}`);
return new BadRequestException('Cannot Create Read Stream');
});
return new StreamableFile(file);
} else if (asset.type == AssetType.VIDEO) {
// Handle Video
let videoPath = asset.originalPath;
let mimeType = asset.mimeType;
if (query.isWeb && asset.mimeType == 'video/quicktime') {
videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath;
mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4';
}
const { size } = await fileInfo(videoPath);
const range = headers.range;
if (range) {
/** Extracting Start and End value from Range Header */
let [start, end] = range.replace(/bytes=/, '').split('-');
start = parseInt(start, 10);
end = end ? parseInt(end, 10) : size - 1;
if (!isNaN(start) && isNaN(end)) {
start = start;
end = size - 1;
}
if (isNaN(start) && !isNaN(end)) {
start = size - end;
end = size - 1;
}
// Handle unavailable range request
if (start >= size || end >= size) {
console.error('Bad Request');
// Return the 416 Range Not Satisfiable.
res.status(416).set({
'Content-Range': `bytes */${size}`,
}); });
throw new BadRequestException('Bad Request Range'); return new StreamableFile(createReadStream(videoPath));
} }
} catch (e) {
/** Sending Partial Content With HTTP Code 206 */ Logger.error('Error serving VIDEO asset ', e);
throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile');
res.status(206).set({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': mimeType,
});
const videoStream = createReadStream(videoPath, { start: start, end: end });
return new StreamableFile(videoStream);
} else {
res.set({
'Content-Type': mimeType,
});
return new StreamableFile(createReadStream(videoPath));
} }
} }
} }

View file

@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsNotEmpty, IsOptional } from 'class-validator';
import { AssetType } from '../entities/asset.entity'; import { AssetType } from '@app/database/entities/asset.entity';
export class CreateAssetDto { export class CreateAssetDto {
@IsNotEmpty() @IsNotEmpty()

View file

@ -1,4 +1,4 @@
import { AssetEntity } from '../entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
export class GetAllAssetReponseDto { export class GetAllAssetReponseDto {
data: Array<{ date: string; assets: Array<AssetEntity> }>; data: Array<{ date: string; assets: Array<AssetEntity> }>;

View file

@ -7,7 +7,7 @@ import { SignUpDto } from './dto/sign-up.dto';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) { } constructor(private readonly authService: AuthService) {}
@Post('/login') @Post('/login')
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) { async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {

View file

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';

View file

@ -1,7 +1,7 @@
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { UserEntity } from '../user/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { LoginCredentialDto } from './dto/login-credential.dto'; import { LoginCredentialDto } from './dto/login-credential.dto';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtPayloadDto } from './dto/jwt-payload.dto'; import { JwtPayloadDto } from './dto/jwt-payload.dto';

View file

@ -4,7 +4,7 @@ import { Socket, Server } from 'socket.io';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@WebSocketGateway() @WebSocketGateway()

View file

@ -6,7 +6,7 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../../config/jwt.config'; import { jwtConfig } from '../../config/jwt.config';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)], imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],

View file

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { DeviceInfoService } from './device-info.service'; import { DeviceInfoService } from './device-info.service';
import { DeviceInfoController } from './device-info.controller'; import { DeviceInfoController } from './device-info.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { DeviceInfoEntity } from './entities/device-info.entity'; import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([DeviceInfoEntity])], imports: [TypeOrmModule.forFeature([DeviceInfoEntity])],

View file

@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateDeviceInfoDto } from './dto/create-device-info.dto'; import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto'; import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
import { DeviceInfoEntity } from './entities/device-info.entity'; import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
@Injectable() @Injectable()
export class DeviceInfoService { export class DeviceInfoService {

View file

@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsNotEmpty, IsOptional } from 'class-validator';
import { DeviceType } from '../entities/device-info.entity'; import { DeviceType } from '@app/database/entities/device-info.entity';
export class CreateDeviceInfoDto { export class CreateDeviceInfoDto {
@IsNotEmpty() @IsNotEmpty()

View file

@ -1,6 +1,6 @@
import { PartialType } from '@nestjs/mapped-types'; import { PartialType } from '@nestjs/mapped-types';
import { IsOptional } from 'class-validator'; import { IsOptional } from 'class-validator';
import { DeviceType } from '../entities/device-info.entity'; import { DeviceType } from '@app/database/entities/device-info.entity';
import { CreateDeviceInfoDto } from './create-device-info.dto'; import { CreateDeviceInfoDto } from './create-device-info.dto';
export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {} export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {}

View file

@ -4,6 +4,6 @@ import { ServerInfoController } from './server-info.controller';
@Module({ @Module({
controllers: [ServerInfoController], controllers: [ServerInfoController],
providers: [ServerInfoService] providers: [ServerInfoService],
}) })
export class ServerInfoModule {} export class ServerInfoModule {}

View file

@ -1,5 +1,5 @@
import { IsNotEmpty } from 'class-validator'; import { IsNotEmpty } from 'class-validator';
import { AssetEntity } from '../../asset/entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
export class AddAssetsDto { export class AddAssetsDto {
@IsNotEmpty() @IsNotEmpty()

View file

@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsNotEmpty, IsOptional } from 'class-validator';
import { AssetEntity } from '../../asset/entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
export class CreateSharedAlbumDto { export class CreateSharedAlbumDto {
@IsNotEmpty() @IsNotEmpty()

View file

@ -2,11 +2,11 @@ import { Module } from '@nestjs/common';
import { SharingService } from './sharing.service'; import { SharingService } from './sharing.service';
import { SharingController } from './sharing.controller'; import { SharingController } from './sharing.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from '../asset/entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
import { UserEntity } from '../user/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { SharedAlbumEntity } from './entities/shared-album.entity'; import { AssetSharedAlbumEntity } from '@app/database/entities/asset-shared-album.entity';
import { AssetSharedAlbumEntity } from './entities/asset-shared-album.entity'; import { SharedAlbumEntity } from '@app/database/entities/shared-album.entity';
import { UserSharedAlbumEntity } from './entities/user-shared-album.entity'; import { UserSharedAlbumEntity } from '@app/database/entities/user-shared-album.entity';
@Module({ @Module({
imports: [ imports: [

View file

@ -2,13 +2,13 @@ import { BadRequestException, Injectable, NotFoundException, UnauthorizedExcepti
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { getConnection, Repository } from 'typeorm'; import { getConnection, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity } from '../asset/entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
import { UserEntity } from '../user/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { AddAssetsDto } from './dto/add-assets.dto'; import { AddAssetsDto } from './dto/add-assets.dto';
import { CreateSharedAlbumDto } from './dto/create-shared-album.dto'; import { CreateSharedAlbumDto } from './dto/create-shared-album.dto';
import { AssetSharedAlbumEntity } from './entities/asset-shared-album.entity'; import { AssetSharedAlbumEntity } from '@app/database/entities/asset-shared-album.entity';
import { SharedAlbumEntity } from './entities/shared-album.entity'; import { SharedAlbumEntity } from '@app/database/entities/shared-album.entity';
import { UserSharedAlbumEntity } from './entities/user-shared-album.entity'; import { UserSharedAlbumEntity } from '@app/database/entities/user-shared-album.entity';
import _ from 'lodash'; import _ from 'lodash';
import { AddUsersDto } from './dto/add-users.dto'; import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';

View file

@ -1,4 +1,4 @@
import { UserEntity } from '../entities/user.entity'; import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity';
export interface User { export interface User {
id: string; id: string;

View file

@ -1,4 +1,19 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query, UseInterceptors, UploadedFile, Response } from '@nestjs/common'; import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
ValidationPipe,
Put,
Query,
UseInterceptors,
UploadedFile,
Response,
} from '@nestjs/common';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
@ -11,7 +26,7 @@ import { Response as Res } from 'express';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(private readonly userService: UserService) { } constructor(private readonly userService: UserService) {}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get() @Get()
@ -28,14 +43,13 @@ export class UserController {
@Get('/count') @Get('/count')
async getUserCount(@Query('isAdmin') isAdmin: boolean) { async getUserCount(@Query('isAdmin') isAdmin: boolean) {
return await this.userService.getUserCount(isAdmin); return await this.userService.getUserCount(isAdmin);
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put() @Put()
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) { async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
return await this.userService.updateUser(updateUserDto) return await this.userService.updateUser(updateUserDto);
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ -46,9 +60,7 @@ export class UserController {
} }
@Get('/profile-image/:userId') @Get('/profile-image/:userId')
async getProfileImage(@Param('userId') userId: string, async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res) {
@Response({ passthrough: true }) res: Res,
) {
return await this.userService.getUserProfileImage(userId, res); return await this.userService.getUserProfileImage(userId, res);
} }
} }

View file

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@ -13,4 +13,4 @@ import { jwtConfig } from '../../config/jwt.config';
controllers: [UserController], controllers: [UserController],
providers: [UserService, ImmichJwtService], providers: [UserService, ImmichJwtService],
}) })
export class UserModule { } export class UserModule {}

View file

@ -4,7 +4,7 @@ import { Not, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateUserDto } from './dto/create-user.dto'; import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import { Response as Res } from 'express'; import { Response as Res } from 'express';

View file

@ -1,15 +1,13 @@
import { Controller, Get, Res, Headers } from '@nestjs/common'; import { Controller, Get, Res, Headers } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
@Controller() @Controller()
export class AppController { export class AppController {
constructor() { } constructor() {}
@Get() @Get()
async redirectToWebpage(@Res({ passthrough: true }) res: Response, @Headers() headers) { async redirectToWebpage(@Res({ passthrough: true }) res: Response, @Headers() headers) {
const host = headers.host; const host = headers.host;
return res.redirect(`http://${host}:2285`) return res.redirect(`http://${host}:2285`);
} }
} }

Some files were not shown because too many files have changed in this diff Show more