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.
**Task List**
[ ] I have read thoroughly the README setup and installation instructions.
[ ] If my setup is different, I have included my docker-compose file.
[ ] I have included my redacted `.env` file.
[ ] I have included information on my machine, and environment.
- [ ] I have read thoroughly the README setup and installation instructions.
- [ ] If my setup is different, I have included my docker-compose file.
- [ ] I have included my redacted `.env` file.
- [ ] I have included information on my machine, and environment.
**To Reproduce**
Steps to reproduce the behavior:

View file

@ -4,17 +4,16 @@ on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
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
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# ref: "main" # branch
fetch-depth: 0
- name: Set up QEMU
@ -27,23 +26,22 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich
- 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' }}
push: true
tags: |
altran1502/immich-server:latest
build_and_push_microservice_latest:
build_and_push_machine_learning_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# ref: "main" # branch
fetch-depth: 0
- name: Set up QEMU
@ -56,15 +54,15 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Microservices
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0
with:
context: ./microservices
file: ./microservices/Dockerfile
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: |
altran1502/immich-microservices:latest
altran1502/immich-machine-learning:latest
build_and_push_web_latest:
runs-on: ubuntu-latest
@ -72,7 +70,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
# ref: "main" # branch
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
@ -91,6 +88,6 @@ jobs:
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: |
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:
context: ../server
dockerfile: Dockerfile
command: npm run start:dev
command: npm run start:dev immich
expose:
- "3000"
volumes:
@ -23,16 +23,35 @@ services:
networks:
- immich-network
immich-microservices:
image: immich-microservices-dev:1.9.0
immich-machine-learning:
image: immich-machine-learning-dev:1.9.0
build:
context: ../microservices
context: ../machine-learning
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3001"
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
- /usr/src/app/node_modules
env_file:

View file

@ -2,11 +2,8 @@ version: "3.8"
services:
immich-server:
image: immich-server-staging:latest
build:
context: ../server
dockerfile: Dockerfile
entrypoint: ["/bin/sh", "./entrypoint.sh"]
image: altran1502/immich-server:staging
entrypoint: ["/bin/sh", "./start-server.sh"]
expose:
- "3000"
volumes:
@ -23,10 +20,23 @@ services:
restart: always
immich-microservices:
image: immich-microservices-staging:latest
build:
context: ../microservices
dockerfile: Dockerfile
image: altran1502/immich-server:staging
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:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose:
- "3001"
@ -43,12 +53,8 @@ services:
restart: always
immich-web:
image: immich-web-staging:latest
image: altran1502/immich-web:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"]
build:
context: ../web
dockerfile: Dockerfile
target: prod
env_file:
- .env
ports:
@ -57,14 +63,12 @@ services:
- immich-network
restart: always
redis:
container_name: immich_redis
image: redis:6.2
networks:
- immich-network
restart: always
database:
container_name: immich_postgres
@ -82,6 +86,7 @@ services:
- 5432:5432
networks:
- immich-network
restart: always
nginx:
container_name: proxy_nginx
@ -102,4 +107,4 @@ services:
networks:
immich-network:
volumes:
pgdata:
pgdata:

View file

@ -3,7 +3,7 @@ version: "3.8"
services:
immich-server:
image: altran1502/immich-server:latest
entrypoint: ["/bin/sh", "./entrypoint.sh"]
entrypoint: ["/bin/sh", "./start-server.sh"]
expose:
- "3000"
volumes:
@ -20,7 +20,23 @@ services:
restart: always
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"]
expose:
- "3001"
@ -47,14 +63,12 @@ services:
- immich-network
restart: always
redis:
container_name: immich_redis
image: redis:6.2
networks:
- immich-network
restart: always
database:
container_name: immich_postgres
@ -73,7 +87,7 @@ services:
networks:
- immich-network
restart: always
nginx:
container_name: proxy_nginx
image: nginx:latest
@ -93,4 +107,4 @@ services:
networks:
immich-network:
volumes:
pgdata:
pgdata:

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ export class ObjectDetectionController {
private readonly objectDetectionService: ObjectDetectionService,
) { }
@Post('/detectObject')
@Post('/detect-object')
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
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.WRITE_EXTERNAL_STORAGE" />
<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>

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

View file

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

View file

@ -13,3 +13,7 @@ const String savedLoginInfoKey = "immichSavedLoginInfoKey";
// Backup Info
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
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/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/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.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/shared/providers/release_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/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart';
void main() async {
@ -24,6 +27,7 @@ void main() async {
await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
@ -48,10 +52,18 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
case AppLifecycleState.resumed:
debugPrint("[APP STATE] 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(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion();
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
break;
@ -95,6 +107,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
@override
Widget build(BuildContext context) {
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Stack(
@ -121,6 +135,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
),
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();
getMapboxInfo() async {
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
state = state.copyWith(mapboxInfo: mapboxInfoRes);
}
getServerVersion() async {
ServerVersion? serverVersion = await _serverInfoService.getServerVersion();

View file

@ -1,4 +1,5 @@
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/server_version.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
@ -13,15 +14,16 @@ class ServerInfoService {
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 {
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"
source: hosted
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:
dependency: transitive
description:

View file

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

View file

@ -1,4 +1,4 @@
FROM node:16-alpine3.14
FROM node:16-alpine3.14 as core
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 { CreateAssetDto } from './dto/create-asset.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 { Response as Res } from 'express';
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 { SearchAssetDto } from './dto/search-asset.dto';
import { CommunicationGateway } from '../communication/communication.gateway';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
@UseGuards(JwtAuthGuard)
@Controller('asset')
@ -39,7 +41,10 @@ export class AssetController {
private wsCommunicateionGateway: CommunicationGateway,
private assetService: AssetService,
private backgroundTaskService: BackgroundTaskService,
) { }
@InjectQueue('asset-uploaded-queue')
private assetUploadedQueue: Queue,
) {}
@Post('upload')
@UseInterceptors(
@ -61,12 +66,23 @@ export class AssetController {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (uploadFiles.thumbnailData != null && savedAsset) {
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset);
}
const assetWithThumbnail = await this.assetService.updateThumbnailInfo(
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));
} catch (e) {

View file

@ -2,9 +2,7 @@ import { Module } from '@nestjs/common';
import { AssetService } from './asset.service';
import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from './entities/asset.entity';
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { AssetEntity } from '@app/database/entities/asset.entity';
import { BullModule } from '@nestjs/bull';
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
@ -13,29 +11,19 @@ import { CommunicationModule } from '../communication/communication.module';
@Module({
imports: [
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,
TypeOrmModule.forFeature([AssetEntity]),
BullModule.registerQueue({
name: 'asset-uploaded-queue',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
],
controllers: [AssetController],
providers: [AssetService, AssetOptimizeService, BackgroundTaskService],
providers: [AssetService, BackgroundTaskService],
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 { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
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 { createReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
@ -11,7 +11,6 @@ import { Response as Res } from 'express';
import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import ffmpeg from 'fluent-ffmpeg';
const fileInfo = promisify(stat);
@ -20,12 +19,18 @@ export class AssetService {
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
) { }
) {}
public async updateThumbnailInfo(assetId: string, path: string) {
return await this.assetRepository.update(assetId, {
resizePath: path,
});
public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
const updatedAsset = await this.assetRepository
.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) {
@ -66,13 +71,13 @@ export class AssetService {
try {
return await this.assetRepository.find({
where: {
userId: authUser.id
userId: authUser.id,
},
relations: ['exifInfo'],
order: {
createdAt: 'DESC'
}
})
createdAt: 'DESC',
},
});
} catch (e) {
Logger.error(e, 'getAllAssets');
}
@ -101,35 +106,45 @@ export class AssetService {
}
public async downloadFile(query: ServeFileDto, res: Res) {
let file = null;
const asset = await this.findOne(query.did, query.aid);
try {
let file = null;
const asset = await this.findOne(query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) {
const { size } = await fileInfo(asset.originalPath);
res.set({
'Content-Type': asset.mimeType,
'Content-Length': size,
});
file = createReadStream(asset.originalPath);
} else {
const { size } = await fileInfo(asset.resizePath);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': size,
});
file = createReadStream(asset.resizePath);
if (query.isThumb === 'false' || !query.isThumb) {
const { size } = await fileInfo(asset.originalPath);
res.set({
'Content-Type': asset.mimeType,
'Content-Length': size,
});
file = createReadStream(asset.originalPath);
} else {
const { size } = await fileInfo(asset.resizePath);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': size,
});
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) {
const asset = await this.assetRepository.findOne({ id: assetId });
try {
const asset = await this.assetRepository.findOne({ id: assetId });
if (asset.webpPath != '') {
return new StreamableFile(createReadStream(asset.webpPath));
} else {
return new StreamableFile(createReadStream(asset.resizePath));
if (asset.webpPath && asset.webpPath.length > 0) {
return new StreamableFile(createReadStream(asset.webpPath));
} else {
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');
}
// Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
/**
@ -154,97 +168,102 @@ export class AssetService {
return new StreamableFile(createReadStream(asset.resizePath));
}
/**
* Serve thumbnail image for both web and mobile app
*/
if (query.isThumb === 'false' || !query.isThumb) {
res.set({
'Content-Type': asset.mimeType,
});
file = createReadStream(asset.originalPath);
} else {
if (asset.webpPath != '') {
try {
/**
* Serve thumbnail image for both web and mobile app
*/
if (query.isThumb === 'false' || !query.isThumb) {
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 {
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);
} 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}`,
'Content-Type': mimeType,
});
throw new BadRequestException('Bad Request Range');
return new StreamableFile(createReadStream(videoPath));
}
/** 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 {
res.set({
'Content-Type': mimeType,
});
return new StreamableFile(createReadStream(videoPath));
} catch (e) {
Logger.error('Error serving VIDEO asset ', e);
throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile');
}
}
}

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
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 { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
import { JwtModule } from '@nestjs/jwt';

View file

@ -1,7 +1,7 @@
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/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 { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
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 { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity';
import { UserEntity } from '@app/database/entities/user.entity';
import { Repository } from 'typeorm';
@WebSocketGateway()

View file

@ -6,7 +6,7 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../../config/jwt.config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity';
import { UserEntity } from '@app/database/entities/user.entity';
@Module({
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 { DeviceInfoController } from './device-info.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeviceInfoEntity } from './entities/device-info.entity';
import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
@Module({
imports: [TypeOrmModule.forFeature([DeviceInfoEntity])],

View file

@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateDeviceInfoDto } from './dto/create-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()
export class DeviceInfoService {

View file

@ -1,5 +1,5 @@
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 {
@IsNotEmpty()

View file

@ -1,6 +1,6 @@
import { PartialType } from '@nestjs/mapped-types';
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';
export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,13 +2,13 @@ import { BadRequestException, Injectable, NotFoundException, UnauthorizedExcepti
import { InjectRepository } from '@nestjs/typeorm';
import { getConnection, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity } from '../asset/entities/asset.entity';
import { UserEntity } from '../user/entities/user.entity';
import { AssetEntity } from '@app/database/entities/asset.entity';
import { UserEntity } from '@app/database/entities/user.entity';
import { AddAssetsDto } from './dto/add-assets.dto';
import { CreateSharedAlbumDto } from './dto/create-shared-album.dto';
import { AssetSharedAlbumEntity } from './entities/asset-shared-album.entity';
import { SharedAlbumEntity } from './entities/shared-album.entity';
import { UserSharedAlbumEntity } from './entities/user-shared-album.entity';
import { AssetSharedAlbumEntity } from '@app/database/entities/asset-shared-album.entity';
import { SharedAlbumEntity } from '@app/database/entities/shared-album.entity';
import { UserSharedAlbumEntity } from '@app/database/entities/user-shared-album.entity';
import _ from 'lodash';
import { AddUsersDto } from './dto/add-users.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 {
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 { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
@ -11,7 +26,7 @@ import { Response as Res } from 'express';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) { }
constructor(private readonly userService: UserService) {}
@UseGuards(JwtAuthGuard)
@Get()
@ -28,14 +43,13 @@ export class UserController {
@Get('/count')
async getUserCount(@Query('isAdmin') isAdmin: boolean) {
return await this.userService.getUserCount(isAdmin);
}
@UseGuards(JwtAuthGuard)
@Put()
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
return await this.userService.updateUser(updateUserDto)
return await this.userService.updateUser(updateUserDto);
}
@UseGuards(JwtAuthGuard)
@ -46,9 +60,7 @@ export class UserController {
}
@Get('/profile-image/:userId')
async getProfileImage(@Param('userId') userId: string,
@Response({ passthrough: true }) res: Res,
) {
async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: 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 { UserController } from './user.controller';
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 { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtModule } from '@nestjs/jwt';
@ -13,4 +13,4 @@ import { jwtConfig } from '../../config/jwt.config';
controllers: [UserController],
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 { CreateUserDto } from './dto/create-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 { createReadStream } from 'fs';
import { Response as Res } from 'express';

View file

@ -1,15 +1,13 @@
import { Controller, Get, Res, Headers } from '@nestjs/common';
import { Response } from 'express';
@Controller()
export class AppController {
constructor() { }
constructor() {}
@Get()
async redirectToWebpage(@Res({ passthrough: true }) res: Response, @Headers() headers) {
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