mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-07-18 19:36:02 +00:00
refactor of synology part 1
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { authenticate } from '../../middleware/auth';
|
import { authenticate } from '../../middleware/auth';
|
||||||
import { broadcast } from '../../websocket';
|
|
||||||
import { AuthRequest } from '../../types';
|
import { AuthRequest } from '../../types';
|
||||||
import {
|
import {
|
||||||
getSynologySettings,
|
getSynologySettings,
|
||||||
@@ -11,114 +10,87 @@ import {
|
|||||||
syncSynologyAlbumLink,
|
syncSynologyAlbumLink,
|
||||||
searchSynologyPhotos,
|
searchSynologyPhotos,
|
||||||
getSynologyAssetInfo,
|
getSynologyAssetInfo,
|
||||||
pipeSynologyProxy,
|
|
||||||
streamSynologyAsset,
|
streamSynologyAsset,
|
||||||
handleSynologyError,
|
|
||||||
SynologyServiceError,
|
|
||||||
} from '../../services/memories/synologyService';
|
} from '../../services/memories/synologyService';
|
||||||
import { canAccessUserPhoto } from '../../services/memories/helpersService';
|
import { canAccessUserPhoto, handleServiceResult, fail } from '../../services/memories/helpersService';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
function parseStringBodyField(value: unknown): string {
|
function _parseStringBodyField(value: unknown): string {
|
||||||
return String(value ?? '').trim();
|
return String(value ?? '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNumberBodyField(value: unknown, fallback: number): number {
|
function _parseNumberBodyField(value: unknown, fallback: number): number {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isFinite(parsed) ? parsed : fallback;
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/settings', authenticate, async (req: Request, res: Response) => {
|
router.get('/settings', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
try {
|
handleServiceResult(res, await getSynologySettings(authReq.user.id));
|
||||||
res.json(await getSynologySettings(authReq.user.id));
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleSynologyError(res, err, 'Failed to load settings');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/settings', authenticate, async (req: Request, res: Response) => {
|
router.put('/settings', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const body = req.body as Record<string, unknown>;
|
const body = req.body as Record<string, unknown>;
|
||||||
const synology_url = parseStringBodyField(body.synology_url);
|
const synology_url = _parseStringBodyField(body.synology_url);
|
||||||
const synology_username = parseStringBodyField(body.synology_username);
|
const synology_username = _parseStringBodyField(body.synology_username);
|
||||||
const synology_password = parseStringBodyField(body.synology_password);
|
const synology_password = _parseStringBodyField(body.synology_password);
|
||||||
|
|
||||||
if (!synology_url || !synology_username) {
|
if (!synology_url || !synology_username) {
|
||||||
return handleSynologyError(res, new SynologyServiceError(400, 'URL and username are required'), 'Missing required fields');
|
handleServiceResult(res, fail('URL and username are required', 400));
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
try {
|
handleServiceResult(res, await updateSynologySettings(authReq.user.id, synology_url, synology_username, synology_password));
|
||||||
await updateSynologySettings(authReq.user.id, synology_url, synology_username, synology_password);
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleSynologyError(res, err, 'Failed to save settings');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/status', authenticate, async (req: Request, res: Response) => {
|
router.get('/status', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
res.json(await getSynologyStatus(authReq.user.id));
|
handleServiceResult(res, await getSynologyStatus(authReq.user.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/test', authenticate, async (req: Request, res: Response) => {
|
router.post('/test', authenticate, async (req: Request, res: Response) => {
|
||||||
const body = req.body as Record<string, unknown>;
|
const body = req.body as Record<string, unknown>;
|
||||||
const synology_url = parseStringBodyField(body.synology_url);
|
const synology_url = _parseStringBodyField(body.synology_url);
|
||||||
const synology_username = parseStringBodyField(body.synology_username);
|
const synology_username = _parseStringBodyField(body.synology_username);
|
||||||
const synology_password = parseStringBodyField(body.synology_password);
|
const synology_password = _parseStringBodyField(body.synology_password);
|
||||||
|
|
||||||
if (!synology_url || !synology_username || !synology_password) {
|
if (!synology_url || !synology_username || !synology_password) {
|
||||||
return handleSynologyError(res, new SynologyServiceError(400, 'URL, username and password are required'), 'Missing required fields');
|
handleServiceResult(res, fail('URL, username, and password are required', 400));
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
handleServiceResult(res, await testSynologyConnection(synology_url, synology_username, synology_password));
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(await testSynologyConnection(synology_url, synology_username, synology_password));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
try {
|
handleServiceResult(res, await listSynologyAlbums(authReq.user.id));
|
||||||
res.json(await listSynologyAlbums(authReq.user.id));
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleSynologyError(res, err, 'Could not reach Synology');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId, linkId } = req.params;
|
const { tripId, linkId } = req.params;
|
||||||
|
|
||||||
try {
|
handleServiceResult(res, await syncSynologyAlbumLink(authReq.user.id, tripId, linkId));
|
||||||
const result = await syncSynologyAlbumLink(authReq.user.id, tripId, linkId);
|
|
||||||
res.json({ success: true, ...result });
|
|
||||||
if (result.added > 0) {
|
|
||||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleSynologyError(res, err, 'Could not reach Synology');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/search', authenticate, async (req: Request, res: Response) => {
|
router.post('/search', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const body = req.body as Record<string, unknown>;
|
const body = req.body as Record<string, unknown>;
|
||||||
const from = parseStringBodyField(body.from);
|
const from = _parseStringBodyField(body.from);
|
||||||
const to = parseStringBodyField(body.to);
|
const to = _parseStringBodyField(body.to);
|
||||||
const offset = parseNumberBodyField(body.offset, 0);
|
const offset = _parseNumberBodyField(body.offset, 0);
|
||||||
const limit = parseNumberBodyField(body.limit, 300);
|
const limit = _parseNumberBodyField(body.limit, 100);
|
||||||
|
|
||||||
try {
|
handleServiceResult(res, await searchSynologyPhotos(
|
||||||
const result = await searchSynologyPhotos(
|
authReq.user.id,
|
||||||
authReq.user.id,
|
from || undefined,
|
||||||
from || undefined,
|
to || undefined,
|
||||||
to || undefined,
|
offset,
|
||||||
offset,
|
limit,
|
||||||
limit,
|
));
|
||||||
);
|
|
||||||
res.json(result);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleSynologyError(res, err, 'Could not reach Synology');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/assets/:tripId/:photoId/:ownerId/info', authenticate, async (req: Request, res: Response) => {
|
router.get('/assets/:tripId/:photoId/:ownerId/info', authenticate, async (req: Request, res: Response) => {
|
||||||
@@ -126,53 +98,29 @@ router.get('/assets/:tripId/:photoId/:ownerId/info', authenticate, async (req: R
|
|||||||
const { tripId, photoId, ownerId } = req.params;
|
const { tripId, photoId, ownerId } = req.params;
|
||||||
|
|
||||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
||||||
return handleSynologyError(res, new SynologyServiceError(403, 'You don\'t have access to this photo'), 'Access denied');
|
handleServiceResult(res, fail('You don\'t have access to this photo', 403));
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
try {
|
handleServiceResult(res, await getSynologyAssetInfo(authReq.user.id, photoId, Number(ownerId)));
|
||||||
res.json(await getSynologyAssetInfo(authReq.user.id, photoId, Number(ownerId)));
|
|
||||||
} catch (err: unknown) {
|
|
||||||
handleSynologyError(res, err, 'Could not reach Synology');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/assets/:tripId/:photoId/:ownerId/thumbnail', authenticate, async (req: Request, res: Response) => {
|
router.get('/assets/:tripId/:photoId/:ownerId/:kind', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId, photoId, ownerId } = req.params;
|
const { tripId, photoId, ownerId, kind } = req.params;
|
||||||
const { size = 'sm' } = req.query;
|
const { size = 'sm' } = req.query;
|
||||||
|
|
||||||
|
if (kind !== 'thumbnail' && kind !== 'original') {
|
||||||
|
handleServiceResult(res, fail('Invalid asset kind', 400));
|
||||||
|
}
|
||||||
|
|
||||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
||||||
return handleSynologyError(res, new SynologyServiceError(403, 'You don\'t have access to this photo'), 'Access denied');
|
handleServiceResult(res, fail('You don\'t have access to this photo', 403));
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
await streamSynologyAsset(res, authReq.user.id, Number(ownerId), photoId, kind as 'thumbnail' | 'original', String(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const proxy = await streamSynologyAsset(authReq.user.id, Number(ownerId), photoId, 'thumbnail', String(size));
|
|
||||||
await pipeSynologyProxy(res, proxy);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (res.headersSent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleSynologyError(res, err, 'Proxy error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/assets/:tripId/:photoId/:ownerId/original', authenticate, async (req: Request, res: Response) => {
|
|
||||||
const authReq = req as AuthRequest;
|
|
||||||
const { tripId, photoId, ownerId } = req.params;
|
|
||||||
|
|
||||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
|
||||||
return handleSynologyError(res, new SynologyServiceError(403, 'You don\'t have access to this photo'), 'Access denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const proxy = await streamSynologyAsset(authReq.user.id, Number(ownerId), photoId, 'original');
|
|
||||||
await pipeSynologyProxy(res, proxy);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (res.headersSent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleSynologyError(res, err, 'Proxy error');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { Readable } from 'node:stream';
|
||||||
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
import { Response } from 'express';
|
||||||
import { canAccessTrip, db } from "../../db/database";
|
import { canAccessTrip, db } from "../../db/database";
|
||||||
|
|
||||||
// helpers for handling return types
|
// helpers for handling return types
|
||||||
@@ -24,6 +27,16 @@ export function mapDbError(error: unknown, fallbackMessage: string): ServiceErro
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function handleServiceResult<T>(res: Response, result: ServiceResult<T>): void {
|
||||||
|
if ('error' in result) {
|
||||||
|
res.status(result.error.status).json({ error: result.error.message });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Service result data:', result.data);
|
||||||
|
res.json(result.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------
|
// ----------------------------------------------
|
||||||
// types used across memories services
|
// types used across memories services
|
||||||
export type Selection = {
|
export type Selection = {
|
||||||
@@ -31,6 +44,29 @@ export type Selection = {
|
|||||||
asset_ids: string[];
|
asset_ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StatusResult = {
|
||||||
|
connected: true;
|
||||||
|
user: { name: string }
|
||||||
|
} | {
|
||||||
|
connected: false;
|
||||||
|
error: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AlbumsList = {
|
||||||
|
albums: Array<{ id: string; albumName: string; assetCount: number }>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetInfo = {
|
||||||
|
id: string;
|
||||||
|
takenAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetsList = {
|
||||||
|
assets: AssetInfo[],
|
||||||
|
total: number,
|
||||||
|
hasMore: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
//for loading routes to settings page, and validating which services user has connected
|
//for loading routes to settings page, and validating which services user has connected
|
||||||
type PhotoProviderConfig = {
|
type PhotoProviderConfig = {
|
||||||
@@ -96,3 +132,21 @@ export function getAlbumIdFromLink(tripId: string, linkId: string, userId: numbe
|
|||||||
export function updateSyncTimeForAlbumLink(linkId: string): void {
|
export function updateSyncTimeForAlbumLink(linkId: string): void {
|
||||||
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function pipeAsset(url: string, response: Response): Promise<void> {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
|
||||||
|
response.status(resp.status);
|
||||||
|
if (resp.headers.get('content-type')) response.set('Content-Type', resp.headers.get('content-type') as string);
|
||||||
|
if (resp.headers.get('cache-control')) response.set('Cache-Control', resp.headers.get('cache-control') as string);
|
||||||
|
if (resp.headers.get('content-length')) response.set('Content-Length', resp.headers.get('content-length') as string);
|
||||||
|
if (resp.headers.get('content-disposition')) response.set('Content-Disposition', resp.headers.get('content-disposition') as string);
|
||||||
|
|
||||||
|
if (!resp.body) {
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pipeline(Readable.fromWeb(resp.body), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
import { Readable } from 'node:stream';
|
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { Response } from 'express';
|
||||||
import { Response as ExpressResponse } from 'express';
|
|
||||||
import { db } from '../../db/database';
|
import { db } from '../../db/database';
|
||||||
import { decrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto';
|
import { decrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto';
|
||||||
import { checkSsrf } from '../../utils/ssrfGuard';
|
import { checkSsrf } from '../../utils/ssrfGuard';
|
||||||
import { addTripPhotos} from './unifiedService';
|
import { addTripPhotos } from './unifiedService';
|
||||||
import { getAlbumIdFromLink, updateSyncTimeForAlbumLink, Selection } from './helpersService';
|
import {
|
||||||
|
getAlbumIdFromLink,
|
||||||
|
updateSyncTimeForAlbumLink,
|
||||||
|
Selection,
|
||||||
|
ServiceResult,
|
||||||
|
fail,
|
||||||
|
success,
|
||||||
|
handleServiceResult,
|
||||||
|
pipeAsset,
|
||||||
|
AlbumsList,
|
||||||
|
AssetsList,
|
||||||
|
StatusResult
|
||||||
|
} from './helpersService';
|
||||||
|
|
||||||
const SYNOLOGY_API_TIMEOUT_MS = 30000;
|
const SYNOLOGY_API_TIMEOUT_MS = 30000;
|
||||||
const SYNOLOGY_PROVIDER = 'synologyphotos';
|
const SYNOLOGY_PROVIDER = 'synologyphotos';
|
||||||
@@ -18,12 +29,6 @@ interface SynologyCredentials {
|
|||||||
synology_password: string;
|
synology_password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SynologySession {
|
|
||||||
success: boolean;
|
|
||||||
sid?: string;
|
|
||||||
error?: { code: number; message?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiCallParams {
|
interface ApiCallParams {
|
||||||
api: string;
|
api: string;
|
||||||
method: string;
|
method: string;
|
||||||
@@ -34,16 +39,7 @@ interface ApiCallParams {
|
|||||||
interface SynologyApiResponse<T> {
|
interface SynologyApiResponse<T> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: T;
|
data?: T;
|
||||||
error?: { code: number; message?: string };
|
error?: { code: number };
|
||||||
}
|
|
||||||
|
|
||||||
export class SynologyServiceError extends Error {
|
|
||||||
status: number;
|
|
||||||
|
|
||||||
constructor(status: number, message: string) {
|
|
||||||
super(message);
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SynologySettings {
|
export interface SynologySettings {
|
||||||
@@ -52,12 +48,6 @@ export interface SynologySettings {
|
|||||||
connected: boolean;
|
connected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SynologyConnectionResult {
|
|
||||||
connected: boolean;
|
|
||||||
user?: { username: string };
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SynologyAlbumLinkInput {
|
export interface SynologyAlbumLinkInput {
|
||||||
album_id?: string | number;
|
album_id?: string | number;
|
||||||
album_name?: string;
|
album_name?: string;
|
||||||
@@ -132,43 +122,50 @@ type SynologyUserRecord = {
|
|||||||
synology_sid?: string | null;
|
synology_sid?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function readSynologyUser(userId: number, columns: string[]): SynologyUserRecord | null {
|
function _readSynologyUser(userId: number, columns: string[]): ServiceResult<SynologyUserRecord> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!columns) return null;
|
if (!columns) return null;
|
||||||
|
|
||||||
const row = db.prepare(`SELECT synology_url, synology_username, synology_password, synology_sid FROM users WHERE id = ?`).get(userId) as SynologyUserRecord | undefined;
|
const row = db.prepare(`SELECT synology_url, synology_username, synology_password, synology_sid FROM users WHERE id = ?`).get(userId) as SynologyUserRecord | undefined;
|
||||||
|
|
||||||
if (!row) return null;
|
if (!row) {
|
||||||
|
return fail('User not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
const filtered: SynologyUserRecord = {};
|
const filtered: SynologyUserRecord = {};
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
filtered[column] = row[column];
|
filtered[column] = row[column];
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered || null;
|
if (!filtered) {
|
||||||
|
return fail('Failed to read Synology user data', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(filtered);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return fail('Failed to read Synology user data', 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSynologyCredentials(userId: number): SynologyCredentials | null {
|
function _getSynologyCredentials(userId: number): ServiceResult<SynologyCredentials> {
|
||||||
const user = readSynologyUser(userId, ['synology_url', 'synology_username', 'synology_password']);
|
const user = _readSynologyUser(userId, ['synology_url', 'synology_username', 'synology_password']);
|
||||||
if (!user?.synology_url || !user.synology_username || !user.synology_password) return null;
|
if (!user.success) return user as ServiceResult<SynologyCredentials>;
|
||||||
return {
|
if (!user?.data.synology_url || !user.data.synology_username || !user.data.synology_password) return fail('Synology not configured', 400);
|
||||||
synology_url: user.synology_url,
|
return success({
|
||||||
synology_username: user.synology_username,
|
synology_url: user.data.synology_url,
|
||||||
synology_password: decrypt_api_key(user.synology_password) as string,
|
synology_username: user.data.synology_username,
|
||||||
};
|
synology_password: decrypt_api_key(user.data.synology_password) as string,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildSynologyEndpoint(url: string): string {
|
function _buildSynologyEndpoint(url: string): string {
|
||||||
const normalized = url.replace(/\/$/, '').match(/^https?:\/\//) ? url.replace(/\/$/, '') : `https://${url.replace(/\/$/, '')}`;
|
const normalized = url.replace(/\/$/, '').match(/^https?:\/\//) ? url.replace(/\/$/, '') : `https://${url.replace(/\/$/, '')}`;
|
||||||
return `${normalized}${SYNOLOGY_ENDPOINT_PATH}`;
|
return `${normalized}${SYNOLOGY_ENDPOINT_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSynologyFormBody(params: ApiCallParams): URLSearchParams {
|
function _buildSynologyFormBody(params: ApiCallParams): URLSearchParams {
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
if (value === undefined || value === null) continue;
|
if (value === undefined || value === null) continue;
|
||||||
@@ -177,8 +174,8 @@ function buildSynologyFormBody(params: ApiCallParams): URLSearchParams {
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSynologyJson<T>(url: string, body: URLSearchParams): Promise<SynologyApiResponse<T>> {
|
async function _fetchSynologyJson<T>(url: string, body: URLSearchParams): Promise<ServiceResult<T>> {
|
||||||
const endpoint = buildSynologyEndpoint(url);
|
const endpoint = _buildSynologyEndpoint(url);
|
||||||
const resp = await fetch(endpoint, {
|
const resp = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -189,14 +186,14 @@ async function fetchSynologyJson<T>(url: string, body: URLSearchParams): Promise
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const text = await resp.text();
|
return fail('Synology API request failed with status ' + resp.status, resp.status);
|
||||||
return { success: false, error: { code: resp.status, message: text } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp.json() as Promise<SynologyApiResponse<T>>;
|
const response = await resp.json() as SynologyApiResponse<T>;
|
||||||
|
return response.success ? success(response.data) : fail('Synology failed with code ' + response.error.code, response.error.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loginToSynology(url: string, username: string, password: string): Promise<SynologyApiResponse<{ sid?: string }>> {
|
async function _loginToSynology(url: string, username: string, password: string): Promise<ServiceResult<string>> {
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
api: 'SYNO.API.Auth',
|
api: 'SYNO.API.Auth',
|
||||||
method: 'login',
|
method: 'login',
|
||||||
@@ -205,40 +202,43 @@ async function loginToSynology(url: string, username: string, password: string):
|
|||||||
passwd: password,
|
passwd: password,
|
||||||
});
|
});
|
||||||
|
|
||||||
return fetchSynologyJson<{ sid?: string }>(url, body);
|
const result = await _fetchSynologyJson<{ sid?: string }>(url, body);
|
||||||
|
if (!result.success) {
|
||||||
|
return result as ServiceResult<string>;
|
||||||
|
}
|
||||||
|
if (!result.data.sid) {
|
||||||
|
return fail('Failed to get session ID from Synology', 500);
|
||||||
|
}
|
||||||
|
return success(result.data.sid);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestSynologyApi<T>(userId: number, params: ApiCallParams): Promise<SynologyApiResponse<T>> {
|
async function _requestSynologyApi<T>(userId: number, params: ApiCallParams): Promise<ServiceResult<T>> {
|
||||||
const creds = getSynologyCredentials(userId);
|
const creds = _getSynologyCredentials(userId);
|
||||||
if (!creds) {
|
if (!creds.success) {
|
||||||
return { success: false, error: { code: 400, message: 'Synology not configured' } };
|
return creds as ServiceResult<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await getSynologySession(userId);
|
const session = await _getSynologySession(userId);
|
||||||
if (!session.success || !session.sid) {
|
if (!session.success || !session.data) {
|
||||||
return { success: false, error: session.error || { code: 400, message: 'Failed to get Synology session' } };
|
return session as ServiceResult<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = buildSynologyFormBody({ ...params, _sid: session.sid });
|
const body = _buildSynologyFormBody({ ...params, _sid: session.data });
|
||||||
const result = await fetchSynologyJson<T>(creds.synology_url, body);
|
const result = await _fetchSynologyJson<T>(creds.data.synology_url, body);
|
||||||
if (!result.success && result.error?.code === 119) {
|
if ('error' in result && result.error.status === 119) {
|
||||||
clearSynologySID(userId);
|
_clearSynologySID(userId);
|
||||||
const retrySession = await getSynologySession(userId);
|
const retrySession = await _getSynologySession(userId);
|
||||||
if (!retrySession.success || !retrySession.sid) {
|
if (!retrySession.success || !retrySession.data) {
|
||||||
return { success: false, error: retrySession.error || { code: 400, message: 'Failed to get Synology session' } };
|
return session as ServiceResult<T>;
|
||||||
}
|
}
|
||||||
return fetchSynologyJson<T>(creds.synology_url, buildSynologyFormBody({ ...params, _sid: retrySession.sid }));
|
return _fetchSynologyJson<T>(creds.data.synology_url, _buildSynologyFormBody({ ...params, _sid: retrySession.data }));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestSynologyStream(url: string): Promise<globalThis.Response> {
|
function _normalizeSynologyPhotoInfo(item: SynologyPhotoItem): SynologyPhotoInfo {
|
||||||
return fetch(url, {
|
|
||||||
signal: AbortSignal.timeout(SYNOLOGY_API_TIMEOUT_MS),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeSynologyPhotoInfo(item: SynologyPhotoItem): SynologyPhotoInfo {
|
|
||||||
const address = item.additional?.address || {};
|
const address = item.additional?.address || {};
|
||||||
const exif = item.additional?.exif || {};
|
const exif = item.additional?.exif || {};
|
||||||
const gps = item.additional?.gps || {};
|
const gps = item.additional?.gps || {};
|
||||||
@@ -268,69 +268,65 @@ function normalizeSynologyPhotoInfo(item: SynologyPhotoItem): SynologyPhotoInfo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleSynologyError(res: ExpressResponse, err: unknown, fallbackMessage: string): ExpressResponse {
|
function _cacheSynologySID(userId: number, sid: string): void {
|
||||||
if (err instanceof SynologyServiceError) {
|
|
||||||
return res.status(err.status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
return res.status(502).json({ error: err instanceof Error ? err.message : fallbackMessage });
|
|
||||||
}
|
|
||||||
|
|
||||||
function cacheSynologySID(userId: number, sid: string): void {
|
|
||||||
db.prepare('UPDATE users SET synology_sid = ? WHERE id = ?').run(sid, userId);
|
db.prepare('UPDATE users SET synology_sid = ? WHERE id = ?').run(sid, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSynologySID(userId: number): void {
|
function _clearSynologySID(userId: number): void {
|
||||||
db.prepare('UPDATE users SET synology_sid = NULL WHERE id = ?').run(userId);
|
db.prepare('UPDATE users SET synology_sid = NULL WHERE id = ?').run(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitPackedSynologyId(rawId: string): { id: string; cacheKey: string; assetId: string } {
|
function _splitPackedSynologyId(rawId: string): { id: string; cacheKey: string; assetId: string } {
|
||||||
const id = rawId.split('_')[0];
|
const id = rawId.split('_')[0];
|
||||||
return { id, cacheKey: rawId, assetId: rawId };
|
return { id, cacheKey: rawId, assetId: rawId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getSynologySession(userId: number): Promise<SynologySession> {
|
async function _getSynologySession(userId: number): Promise<ServiceResult<string>> {
|
||||||
const cachedSid = readSynologyUser(userId, ['synology_sid'])?.synology_sid || null;
|
const cachedSid = _readSynologyUser(userId, ['synology_sid']);
|
||||||
if (cachedSid) {
|
if (cachedSid.success && cachedSid.data?.synology_sid) {
|
||||||
return { success: true, sid: cachedSid };
|
return success(cachedSid.data.synology_sid);
|
||||||
}
|
|
||||||
|
|
||||||
const creds = getSynologyCredentials(userId);
|
|
||||||
if (!creds) {
|
|
||||||
return { success: false, error: { code: 400, message: 'Invalid Synology credentials' } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await loginToSynology(creds.synology_url, creds.synology_username, creds.synology_password);
|
const creds = _getSynologyCredentials(userId);
|
||||||
|
if (!creds.success) {
|
||||||
if (!resp.success || !resp.data?.sid) {
|
return creds as ServiceResult<string>;
|
||||||
return { success: false, error: resp.error || { code: 400, message: 'Failed to authenticate with Synology' } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheSynologySID(userId, resp.data.sid);
|
const resp = await _loginToSynology(creds.data.synology_url, creds.data.synology_username, creds.data.synology_password);
|
||||||
return { success: true, sid: resp.data.sid };
|
|
||||||
|
if (!resp.success) {
|
||||||
|
return resp as ServiceResult<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cacheSynologySID(userId, resp.data);
|
||||||
|
return success(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSynologySettings(userId: number): Promise<SynologySettings> {
|
export async function getSynologySettings(userId: number): Promise<ServiceResult<SynologySettings>> {
|
||||||
const creds = getSynologyCredentials(userId);
|
const creds = _getSynologyCredentials(userId);
|
||||||
const session = await getSynologySession(userId);
|
if (!creds.success) return creds as ServiceResult<SynologySettings>;
|
||||||
return {
|
const session = await _getSynologySession(userId);
|
||||||
synology_url: creds?.synology_url || '',
|
return success({
|
||||||
synology_username: creds?.synology_username || '',
|
synology_url: creds.data.synology_url || '',
|
||||||
|
synology_username: creds.data.synology_username || '',
|
||||||
connected: session.success,
|
connected: session.success,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSynologySettings(userId: number, synologyUrl: string, synologyUsername: string, synologyPassword?: string): Promise<void> {
|
export async function updateSynologySettings(userId: number, synologyUrl: string, synologyUsername: string, synologyPassword?: string): Promise<ServiceResult<string>> {
|
||||||
|
|
||||||
const ssrf = await checkSsrf(synologyUrl);
|
const ssrf = await checkSsrf(synologyUrl);
|
||||||
if (!ssrf.allowed) {
|
if (!ssrf.allowed) {
|
||||||
throw new SynologyServiceError(400, ssrf.error ?? 'Invalid Synology URL');
|
return fail(ssrf.error, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingEncryptedPassword = readSynologyUser(userId, ['synology_password'])?.synology_password || null;
|
const result = _readSynologyUser(userId, ['synology_password'])
|
||||||
|
if (!result.success) return result as ServiceResult<string>;
|
||||||
|
const existingEncryptedPassword = result.data?.synology_password || null;
|
||||||
|
|
||||||
if (!synologyPassword && !existingEncryptedPassword) {
|
if (!synologyPassword && !existingEncryptedPassword) {
|
||||||
throw new SynologyServiceError(400, 'No stored password found. Please provide a password to save settings.');
|
return fail('No stored password found. Please provide a password to save settings.', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -341,79 +337,69 @@ export async function updateSynologySettings(userId: number, synologyUrl: string
|
|||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
throw new SynologyServiceError(400, 'Failed to save settings');
|
return fail('Failed to update Synology settings', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSynologySID(userId);
|
_clearSynologySID(userId);
|
||||||
await getSynologySession(userId);
|
return success("settings updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSynologyStatus(userId: number): Promise<SynologyConnectionResult> {
|
export async function getSynologyStatus(userId: number): Promise<ServiceResult<StatusResult>> {
|
||||||
|
const sid = await _getSynologySession(userId);
|
||||||
|
if ('error' in sid) return success({ connected: false, error: sid.error.status === 400 ? 'Invalid credentials' : sid.error.message });
|
||||||
|
if (!sid.data) return success({ connected: false, error: 'Not connected to Synology' });
|
||||||
try {
|
try {
|
||||||
const sid = await getSynologySession(userId);
|
|
||||||
if (!sid.success || !sid.sid) {
|
|
||||||
return { connected: false, error: 'Authentication failed' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = db.prepare('SELECT synology_username FROM users WHERE id = ?').get(userId) as { synology_username?: string } | undefined;
|
const user = db.prepare('SELECT synology_username FROM users WHERE id = ?').get(userId) as { synology_username?: string } | undefined;
|
||||||
return { connected: true, user: { username: user?.synology_username || '' } };
|
return success({ connected: true, user: { name: user?.synology_username || 'unknown user' } });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return { connected: false, error: err instanceof Error ? err.message : 'Connection failed' };
|
return success({ connected: true, user: { name: 'unknown user' } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testSynologyConnection(synologyUrl: string, synologyUsername: string, synologyPassword: string): Promise<SynologyConnectionResult> {
|
export async function testSynologyConnection(synologyUrl: string, synologyUsername: string, synologyPassword: string): Promise<ServiceResult<StatusResult>> {
|
||||||
|
|
||||||
const ssrf = await checkSsrf(synologyUrl);
|
const ssrf = await checkSsrf(synologyUrl);
|
||||||
if (!ssrf.allowed) {
|
if (!ssrf.allowed) {
|
||||||
return { connected: false, error: ssrf.error ?? 'Invalid Synology URL' };
|
return fail(ssrf.error, 400);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const login = await loginToSynology(synologyUrl, synologyUsername, synologyPassword);
|
const resp = await _loginToSynology(synologyUrl, synologyUsername, synologyPassword);
|
||||||
if (!login.success || !login.data?.sid) {
|
if ('error' in resp) {
|
||||||
return { connected: false, error: login.error?.message || 'Authentication failed' };
|
return success({ connected: false, error: resp.error.status === 400 ? 'Invalid credentials' : resp.error.message });
|
||||||
}
|
|
||||||
return { connected: true, user: { username: synologyUsername } };
|
|
||||||
} catch (err: unknown) {
|
|
||||||
return { connected: false, error: err instanceof Error ? err.message : 'Connection failed' };
|
|
||||||
}
|
}
|
||||||
|
return success({ connected: true, user: { name: synologyUsername } });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSynologyAlbums(userId: number): Promise<{ albums: Array<{ id: string; albumName: string; assetCount: number }> }> {
|
export async function listSynologyAlbums(userId: number): Promise<ServiceResult<AlbumsList>> {
|
||||||
const result = await requestSynologyApi<{ list: SynologyPhotoItem[] }>(userId, {
|
const result = await _requestSynologyApi<{ list: SynologyPhotoItem[] }>(userId, {
|
||||||
api: 'SYNO.Foto.Browse.Album',
|
api: 'SYNO.Foto.Browse.Album',
|
||||||
method: 'list',
|
method: 'list',
|
||||||
version: 4,
|
version: 4,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
if (!result.success) return result as ServiceResult<AlbumsList>;
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
const albums = (result.data.list || []).map((album: any) => ({
|
||||||
throw new SynologyServiceError(result.error?.code || 500, result.error?.message || 'Failed to fetch albums');
|
|
||||||
}
|
|
||||||
|
|
||||||
const albums = (result.data.list || []).map((album: SynologyPhotoItem) => ({
|
|
||||||
id: String(album.id),
|
id: String(album.id),
|
||||||
albumName: album.name || '',
|
albumName: album.name || '',
|
||||||
assetCount: album.item_count || 0,
|
assetCount: album.item_count || 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { albums };
|
return success({ albums });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function syncSynologyAlbumLink(userId: number, tripId: string, linkId: string): Promise<{ added: number; total: number }> {
|
export async function syncSynologyAlbumLink(userId: number, tripId: string, linkId: string): Promise<ServiceResult<{ added: number; total: number }>> {
|
||||||
const response = getAlbumIdFromLink(tripId, linkId, userId);
|
const response = getAlbumIdFromLink(tripId, linkId, userId);
|
||||||
if (!response.success) {
|
if (!response.success) return response as ServiceResult<{ added: number; total: number }>;
|
||||||
throw new SynologyServiceError(404, 'Album link not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allItems: SynologyPhotoItem[] = [];
|
const allItems: SynologyPhotoItem[] = [];
|
||||||
const pageSize = 1000;
|
const pageSize = 1000;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await requestSynologyApi<{ list: SynologyPhotoItem[] }>(userId, {
|
const result = await _requestSynologyApi<{ list: SynologyPhotoItem[] }>(userId, {
|
||||||
api: 'SYNO.Foto.Browse.Item',
|
api: 'SYNO.Foto.Browse.Item',
|
||||||
method: 'list',
|
method: 'list',
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -423,9 +409,7 @@ export async function syncSynologyAlbumLink(userId: number, tripId: string, link
|
|||||||
additional: ['thumbnail'],
|
additional: ['thumbnail'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
if (!result.success) return result as ServiceResult<{ added: number; total: number }>;
|
||||||
throw new SynologyServiceError(502, result.error?.message || 'Failed to fetch album');
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = result.data.list || [];
|
const items = result.data.list || [];
|
||||||
allItems.push(...items);
|
allItems.push(...items);
|
||||||
@@ -441,12 +425,12 @@ export async function syncSynologyAlbumLink(userId: number, tripId: string, link
|
|||||||
updateSyncTimeForAlbumLink(linkId);
|
updateSyncTimeForAlbumLink(linkId);
|
||||||
|
|
||||||
const result = await addTripPhotos(tripId, userId, true, [selection]);
|
const result = await addTripPhotos(tripId, userId, true, [selection]);
|
||||||
if ('error' in result) throw new SynologyServiceError(result.error.status, result.error.message);
|
if (!result.success) return result as ServiceResult<{ added: number; total: number }>;
|
||||||
|
|
||||||
return { added: result.data.added, total: allItems.length };
|
return success({ added: result.data.added, total: allItems.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchSynologyPhotos(userId: number, from?: string, to?: string, offset = 0, limit = 300): Promise<{ assets: SynologyPhotoInfo[]; total: number; hasMore: boolean }> {
|
export async function searchSynologyPhotos(userId: number, from?: string, to?: string, offset = 0, limit = 300): Promise<ServiceResult<AssetsList>> {
|
||||||
const params: ApiCallParams = {
|
const params: ApiCallParams = {
|
||||||
api: 'SYNO.Foto.Search.Search',
|
api: 'SYNO.Foto.Search.Search',
|
||||||
method: 'list_item',
|
method: 'list_item',
|
||||||
@@ -466,25 +450,23 @@ export async function searchSynologyPhotos(userId: number, from?: string, to?: s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await requestSynologyApi<{ list: SynologyPhotoItem[]; total: number }>(userId, params);
|
const result = await _requestSynologyApi<{ list: SynologyPhotoItem[]; total: number }>(userId, params);
|
||||||
if (!result.success || !result.data) {
|
if (!result.success) return result as ServiceResult<{ assets: SynologyPhotoInfo[]; total: number; hasMore: boolean }>;
|
||||||
throw new SynologyServiceError(502, result.error?.message || 'Failed to fetch album photos');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allItems = result.data.list || [];
|
const allItems = result.data.list || [];
|
||||||
const total = allItems.length;
|
const total = allItems.length;
|
||||||
const assets = allItems.map(item => normalizeSynologyPhotoInfo(item));
|
const assets = allItems.map(item => _normalizeSynologyPhotoInfo(item));
|
||||||
|
|
||||||
return {
|
return success({
|
||||||
assets,
|
assets,
|
||||||
total,
|
total,
|
||||||
hasMore: total === limit,
|
hasMore: total === limit,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSynologyAssetInfo(userId: number, photoId: string, targetUserId?: number): Promise<SynologyPhotoInfo> {
|
export async function getSynologyAssetInfo(userId: number, photoId: string, targetUserId?: number): Promise<ServiceResult<SynologyPhotoInfo>> {
|
||||||
const parsedId = splitPackedSynologyId(photoId);
|
const parsedId = _splitPackedSynologyId(photoId);
|
||||||
const result = await requestSynologyApi<{ list: SynologyPhotoItem[] }>(targetUserId ?? userId, {
|
const result = await _requestSynologyApi<{ list: SynologyPhotoItem[] }>(targetUserId, {
|
||||||
api: 'SYNO.Foto.Browse.Item',
|
api: 'SYNO.Foto.Browse.Item',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
version: 5,
|
version: 5,
|
||||||
@@ -492,39 +474,41 @@ export async function getSynologyAssetInfo(userId: number, photoId: string, targ
|
|||||||
additional: ['resolution', 'exif', 'gps', 'address', 'orientation', 'description'],
|
additional: ['resolution', 'exif', 'gps', 'address', 'orientation', 'description'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
if (!result.success) return result as ServiceResult<SynologyPhotoInfo>;
|
||||||
throw new SynologyServiceError(404, 'Photo not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = result.data.list?.[0];
|
const metadata = result.data.list?.[0];
|
||||||
if (!metadata) {
|
if (!metadata) return fail('Photo not found', 404);
|
||||||
throw new SynologyServiceError(404, 'Photo not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = normalizeSynologyPhotoInfo(metadata);
|
const normalized = _normalizeSynologyPhotoInfo(metadata);
|
||||||
normalized.id = photoId;
|
normalized.id = photoId;
|
||||||
return normalized;
|
return success(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function streamSynologyAsset(
|
export async function streamSynologyAsset(
|
||||||
|
response: Response,
|
||||||
userId: number,
|
userId: number,
|
||||||
targetUserId: number,
|
targetUserId: number,
|
||||||
photoId: string,
|
photoId: string,
|
||||||
kind: 'thumbnail' | 'original',
|
kind: 'thumbnail' | 'original',
|
||||||
size?: string,
|
size?: string,
|
||||||
): Promise<SynologyProxyResult> {
|
): Promise<void> {
|
||||||
const parsedId = splitPackedSynologyId(photoId);
|
const parsedId = _splitPackedSynologyId(photoId);
|
||||||
const synology_url = getSynologyCredentials(targetUserId).synology_url;
|
|
||||||
if (!synology_url) {
|
const synology_credentials = _getSynologyCredentials(targetUserId);
|
||||||
throw new SynologyServiceError(402, 'User not configured with Synology');
|
if (!synology_credentials.success) {
|
||||||
|
handleServiceResult(response, synology_credentials);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sid = await getSynologySession(targetUserId);
|
const sid = await _getSynologySession(targetUserId);
|
||||||
if (!sid.success || !sid.sid) {
|
if (!sid.success) {
|
||||||
throw new SynologyServiceError(401, 'Authentication failed');
|
handleServiceResult(response, sid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sid.data) {
|
||||||
|
handleServiceResult(response, fail('Failed to retrieve session ID', 500));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const params = kind === 'thumbnail'
|
const params = kind === 'thumbnail'
|
||||||
? new URLSearchParams({
|
? new URLSearchParams({
|
||||||
@@ -536,7 +520,7 @@ export async function streamSynologyAsset(
|
|||||||
type: 'unit',
|
type: 'unit',
|
||||||
size: String(size || SYNOLOGY_DEFAULT_THUMBNAIL_SIZE),
|
size: String(size || SYNOLOGY_DEFAULT_THUMBNAIL_SIZE),
|
||||||
cache_key: parsedId.cacheKey,
|
cache_key: parsedId.cacheKey,
|
||||||
_sid: sid.sid,
|
_sid: sid.data,
|
||||||
})
|
})
|
||||||
: new URLSearchParams({
|
: new URLSearchParams({
|
||||||
api: 'SYNO.Foto.Download',
|
api: 'SYNO.Foto.Download',
|
||||||
@@ -544,40 +528,11 @@ export async function streamSynologyAsset(
|
|||||||
version: '2',
|
version: '2',
|
||||||
cache_key: parsedId.cacheKey,
|
cache_key: parsedId.cacheKey,
|
||||||
unit_id: `[${parsedId.id}]`,
|
unit_id: `[${parsedId.id}]`,
|
||||||
_sid: sid.sid,
|
_sid: sid.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = `${buildSynologyEndpoint(synology_url)}?${params.toString()}`;
|
const url = `${_buildSynologyEndpoint(synology_credentials.data.synology_url)}?${params.toString()}`;
|
||||||
const resp = await requestSynologyStream(url);
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
await pipeAsset(url, response)
|
||||||
const body = kind === 'original' ? await resp.text() : 'Failed';
|
|
||||||
throw new SynologyServiceError(resp.status, kind === 'original' ? `Failed: ${body}` : body);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: resp.status,
|
|
||||||
headers: {
|
|
||||||
'content-type': resp.headers.get('content-type') || (kind === 'thumbnail' ? 'image/jpeg' : 'application/octet-stream'),
|
|
||||||
'cache-control': resp.headers.get('cache-control') || 'public, max-age=86400',
|
|
||||||
'content-length': resp.headers.get('content-length'),
|
|
||||||
'content-disposition': resp.headers.get('content-disposition'),
|
|
||||||
},
|
|
||||||
body: resp.body,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pipeSynologyProxy(response: ExpressResponse, proxy: SynologyProxyResult): Promise<void> {
|
|
||||||
response.status(proxy.status);
|
|
||||||
if (proxy.headers['content-type']) response.set('Content-Type', proxy.headers['content-type'] as string);
|
|
||||||
if (proxy.headers['cache-control']) response.set('Cache-Control', proxy.headers['cache-control'] as string);
|
|
||||||
if (proxy.headers['content-length']) response.set('Content-Length', proxy.headers['content-length'] as string);
|
|
||||||
if (proxy.headers['content-disposition']) response.set('Content-Disposition', proxy.headers['content-disposition'] as string);
|
|
||||||
|
|
||||||
if (!proxy.body) {
|
|
||||||
response.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await pipeline(Readable.fromWeb(proxy.body), response);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user