diff --git a/frontend/src/app/routes/view/view-speeddial/view-speeddial.component.ts b/frontend/src/app/routes/view/view-speeddial/view-speeddial.component.ts
index cd6125b..0cc646d 100644
--- a/frontend/src/app/routes/view/view-speeddial/view-speeddial.component.ts
+++ b/frontend/src/app/routes/view/view-speeddial/view-speeddial.component.ts
@@ -93,10 +93,7 @@ export class ViewSpeeddialComponent implements OnInit {
if (this.image === null) return;
this.downloadService.downloadFile(
- this.imageService.CreateImageLinksFromID(
- this.image?.id,
- this.selectedFormat,
- ).source,
+ this.imageService.GetImageURL(this.image?.id, this.selectedFormat),
);
}
@@ -104,10 +101,7 @@ export class ViewSpeeddialComponent implements OnInit {
if (this.image === null) return;
this.downloadService.shareFile(
- this.imageService.CreateImageLinksFromID(
- this.image?.id,
- this.selectedFormat,
- ).source,
+ this.imageService.GetImageURL(this.image?.id, this.selectedFormat),
);
}
diff --git a/frontend/src/app/services/api/api.service.ts b/frontend/src/app/services/api/api.service.ts
index 831e147..f093848 100644
--- a/frontend/src/app/services/api/api.service.ts
+++ b/frontend/src/app/services/api/api.service.ts
@@ -7,7 +7,7 @@ import {
Fail,
FT,
HasFailed,
- HasSuccess,
+ HasSuccess
} from 'picsur-shared/dist/types';
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime';
@@ -17,7 +17,7 @@ import { ApiError } from 'src/app/models/dto/api-error.dto';
import { z } from 'zod';
import { MultiPartRequest } from '../../models/dto/multi-part-request.dto';
import { Logger } from '../logger/logger.service';
-import { KeyService } from '../storage/key.service';
+import { KeyStorageService } from '../storage/key-storage.service';
/*
Proud of this, it works so smoooth
@@ -36,7 +36,7 @@ export class ApiService {
}
constructor(
- private readonly keyService: KeyService,
+ private readonly keyService: KeyStorageService,
@Inject(WINDOW) private readonly windowRef: Window,
) {}
diff --git a/frontend/src/app/services/api/image.service.ts b/frontend/src/app/services/api/image.service.ts
index 32c86ba..85bcfb0 100644
--- a/frontend/src/app/services/api/image.service.ts
+++ b/frontend/src/app/services/api/image.service.ts
@@ -125,8 +125,9 @@ export class ImageService {
// Non api calls
- public GetImageURL(image: string, filetype: string | null): string {
- const baseURL = this.infoService.getHostname();
+ // Use for native images
+ public GetImageURL(image: string, filetype: string | null, allowOverride = false): string {
+ const baseURL = this.infoService.getHostname(allowOverride);
const extension = FileType2Ext(filetype ?? '');
return `${baseURL}/i/${image}${
@@ -134,12 +135,23 @@ export class ImageService {
}`;
}
+ // Use for user facing urls
+ public CreateImageLinks(imageURL: string): ImageLinks {
+ return {
+ source: imageURL,
+ markdown: `![image](${imageURL})`,
+ html: ``,
+ rst: `.. image:: ${imageURL}`,
+ bbcode: `[img]${imageURL}[/img]`,
+ };
+ }
+
public GetImageURLCustomized(
image: string,
filetype: string | null,
options: ImageRequestParams,
): string {
- const baseURL = this.GetImageURL(image, filetype);
+ const baseURL = this.GetImageURL(image, filetype, true);
const betterOptions = ImageRequestParams.zodSchema.safeParse(options);
if (!betterOptions.success) return baseURL;
@@ -169,20 +181,10 @@ export class ImageService {
return baseURL + '?' + queryParams.join('&');
}
- public CreateImageLinks(imageURL: string): ImageLinks {
- return {
- source: imageURL,
- markdown: `![image](${imageURL})`,
- html: ``,
- rst: `.. image:: ${imageURL}`,
- bbcode: `[img]${imageURL}[/img]`,
- };
- }
-
public CreateImageLinksFromID(
imageID: string,
mime: string | null,
): ImageLinks {
- return this.CreateImageLinks(this.GetImageURL(imageID, mime));
+ return this.CreateImageLinks(this.GetImageURL(imageID, mime, true));
}
}
diff --git a/frontend/src/app/services/api/info.service.ts b/frontend/src/app/services/api/info.service.ts
index 5902383..8eff4fa 100644
--- a/frontend/src/app/services/api/info.service.ts
+++ b/frontend/src/app/services/api/info.service.ts
@@ -7,6 +7,7 @@ import { BehaviorSubject, filter, Observable, take } from 'rxjs';
import pkg from '../../../../package.json';
import { ServerInfo } from '../../models/dto/server-info.dto';
import { Logger } from '../logger/logger.service';
+import { InfoStorageService } from '../storage/info-storage.service';
import { ApiService } from './api.service';
@Injectable({
@@ -23,21 +24,16 @@ export class InfoService {
return this.infoSubject.value;
}
- private infoSubject = new BehaviorSubject(new ServerInfo());
+ private infoSubject = new BehaviorSubject(
+ this.infoStorage.get() ?? new ServerInfo(),
+ );
constructor(
- private readonly api: ApiService,
@Inject(LOCATION) private readonly location: Location,
+ private readonly api: ApiService,
+ private readonly infoStorage: InfoStorageService,
) {
- this.pollInfo().catch((e) => this.logger.warn(e));
- }
-
- public async pollInfo(): AsyncFailable {
- const response = await this.api.get(InfoResponse, '/api/info');
- if (HasFailed(response)) return response;
-
- this.infoSubject.next(response);
- return response;
+ this.updateInfo().catch((e) => this.logger.warn(e));
}
public async getLoadedSnapshot(): Promise {
@@ -58,12 +54,14 @@ export class InfoService {
return pkg.version;
}
- public getHostname(): string {
- // const info = await this.getLoadedSnapshot();
+ public getHostname(allowOverride = false): string {
+ if (allowOverride) {
+ const info = this.snapshot;
- // if (info.host_override !== undefined) {
- // return info.host_override;
- // }
+ if (info.host_override !== undefined) {
+ return info.host_override;
+ }
+ }
return this.location.protocol + '//' + this.location.host;
}
@@ -71,7 +69,7 @@ export class InfoService {
// If either version starts with 0. it has to be exactly the same
// If both versions start with something else, they have to match the first part
public async isCompatibleWithServer(): AsyncFailable {
- const info = await this.pollInfo();
+ const info = await this.getLoadedSnapshot();
if (HasFailed(info)) return info;
const serverVersion = info.version;
@@ -101,4 +99,13 @@ export class InfoService {
public isLoaded(): boolean {
return this.snapshot.version !== '0.0.0';
}
+
+ private async updateInfo(): AsyncFailable {
+ const response = await this.api.get(InfoResponse, '/api/info');
+ if (HasFailed(response)) return response;
+
+ this.infoSubject.next(response);
+ this.infoStorage.set(response);
+ return response;
+ }
}
diff --git a/frontend/src/app/services/api/user.service.ts b/frontend/src/app/services/api/user.service.ts
index 9a8ab90..3b5ad79 100644
--- a/frontend/src/app/services/api/user.service.ts
+++ b/frontend/src/app/services/api/user.service.ts
@@ -7,7 +7,7 @@ import {
UserLoginResponse,
UserMeResponse,
UserRegisterRequest,
- UserRegisterResponse,
+ UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto';
import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
@@ -16,11 +16,11 @@ import {
Fail,
FT,
HasFailed,
- Open,
+ Open
} from 'picsur-shared/dist/types';
import { BehaviorSubject } from 'rxjs';
import { Logger } from '../logger/logger.service';
-import { KeyService } from '../storage/key.service';
+import { KeyStorageService } from '../storage/key-storage.service';
import { ApiService } from './api.service';
@Injectable({
@@ -44,7 +44,7 @@ export class UserService {
constructor(
private readonly api: ApiService,
- private readonly key: KeyService,
+ private readonly key: KeyStorageService,
) {
this.init().catch(this.logger.error);
}
diff --git a/frontend/src/app/services/storage/cache.service.ts b/frontend/src/app/services/storage/cache.service.ts
index 766467c..7803b34 100644
--- a/frontend/src/app/services/storage/cache.service.ts
+++ b/frontend/src/app/services/storage/cache.service.ts
@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@angular/core';
import { SESSION_STORAGE } from '@ng-web-apis/common';
import { AsyncFailable, Failable, HasFailed } from 'picsur-shared/dist/types';
+import { Logger } from '../logger/logger.service';
interface dataWrapper {
data: T;
@@ -11,6 +12,8 @@ interface dataWrapper {
providedIn: 'root',
})
export class CacheService {
+ private readonly logger = new Logger(CacheService.name);
+
private readonly cacheExpiresMS = 1000 * 60 * 60;
private cacheVersion = '0.0.0';
@@ -76,6 +79,35 @@ export class CacheService {
return finalFallback;
}
+ public getFallbackSync(
+ key: string,
+ finalFallback: T,
+ ...fallbacks: Array<(key: string) => AsyncFailable | Failable>
+ ): T {
+ const cached = this.get(key);
+ if (cached !== null) {
+ return cached;
+ }
+
+ const background = async () => {
+ for (const fallback of fallbacks) {
+ const result = await fallback(key);
+ if (HasFailed(result)) {
+ continue;
+ }
+
+ this.set(key, result);
+ return result;
+ }
+
+ return finalFallback;
+ };
+
+ background().catch((e) => this.logger.error(e));
+
+ return finalFallback;
+ }
+
private transformKey(key: string): string {
return `${this.cacheVersion}-${key}`;
}
diff --git a/frontend/src/app/services/storage/info-storage.service.ts b/frontend/src/app/services/storage/info-storage.service.ts
new file mode 100644
index 0000000..7f678d6
--- /dev/null
+++ b/frontend/src/app/services/storage/info-storage.service.ts
@@ -0,0 +1,50 @@
+import { Injectable } from '@angular/core';
+import { ServerInfo } from 'src/app/models/dto/server-info.dto';
+import { Logger } from '../logger/logger.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class InfoStorageService {
+ private readonly logger = new Logger(InfoStorageService.name);
+
+ private readonly storageKey = 'server-info';
+ private info: ServerInfo | null = null;
+
+ constructor() {
+ this.load();
+ }
+
+ private load() {
+ try {
+ const hasRead = localStorage.getItem(this.storageKey);
+ if (hasRead === null) return;
+
+ this.info = JSON.parse(hasRead);
+ } catch (e) {
+ this.logger.warn(e);
+ localStorage.removeItem(this.storageKey);
+ }
+ }
+
+ private store() {
+ if (this.info)
+ localStorage.setItem(this.storageKey, JSON.stringify(this.info));
+ else localStorage.removeItem(this.storageKey);
+ }
+
+ public get() {
+ setTimeout(this.load.bind(this), 0);
+ return this.info;
+ }
+
+ public set(info: ServerInfo) {
+ this.info = info;
+ this.store();
+ }
+
+ public clear() {
+ this.info = null;
+ this.store();
+ }
+}
diff --git a/frontend/src/app/services/storage/key.service.ts b/frontend/src/app/services/storage/key-storage.service.ts
similarity index 94%
rename from frontend/src/app/services/storage/key.service.ts
rename to frontend/src/app/services/storage/key-storage.service.ts
index 50d0b15..d60f1ed 100644
--- a/frontend/src/app/services/storage/key.service.ts
+++ b/frontend/src/app/services/storage/key-storage.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
-export class KeyService {
+export class KeyStorageService {
private key: string | null = null;
constructor() {
diff --git a/frontend/src/app/workers/qoi-worker.service.ts b/frontend/src/app/workers/qoi-worker.service.ts
index f3efe0c..733510b 100644
--- a/frontend/src/app/workers/qoi-worker.service.ts
+++ b/frontend/src/app/workers/qoi-worker.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { AsyncFailable, Failure, HasFailed } from 'picsur-shared/dist/types';
-import { KeyService } from '../services/storage/key.service';
+import { KeyStorageService } from '../services/storage/key-storage.service';
import { QOIImage, QOIJob, QOIWorkerOut } from './qoi-worker.dto';
@Injectable({
@@ -10,7 +10,7 @@ export class QoiWorkerService {
private worker: Worker | null = null;
private job: Promise | null = null;
- constructor(private readonly keyService: KeyService) {
+ constructor(private readonly keyService: KeyStorageService) {
if (typeof Worker !== 'undefined') {
this.worker = new Worker(new URL('./qoi.worker', import.meta.url));
} else {
diff --git a/shared/src/dto/sys-preferences.enum.ts b/shared/src/dto/sys-preferences.enum.ts
index 5bc4536..f75f650 100644
--- a/shared/src/dto/sys-preferences.enum.ts
+++ b/shared/src/dto/sys-preferences.enum.ts
@@ -1,5 +1,5 @@
import { z } from 'zod';
-import { HostNameRegex, URLRegex } from '../util/common-regex';
+import { URLRegex } from '../util/common-regex';
import { IsEntityID } from '../validators/entity-id.validator';
import { IsValidMS } from '../validators/ms.validator';
import { IsPosInt } from '../validators/positive-int.validator';
@@ -57,10 +57,7 @@ export const SysPreferenceValueTypes: {
export const SysPreferenceValidators: {
[key in SysPreference]: z.ZodTypeAny;
} = {
- [SysPreference.HostOverride]: z
- .string()
- .regex(HostNameRegex)
- .or(z.literal('')),
+ [SysPreference.HostOverride]: z.string().regex(URLRegex).or(z.literal('')),
[SysPreference.JwtSecret]: z.boolean(),
[SysPreference.JwtExpiresIn]: IsValidMS(),