Skip to content

Commit 992de5a

Browse files
excitonshenlong-tanwen
authored andcommitted
feat: add album start and end dates for storage template (#17188)
1 parent 051e6e7 commit 992de5a

File tree

4 files changed

+102
-3
lines changed

4 files changed

+102
-3
lines changed

server/src/services/storage-template.service.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ describe(StorageTemplateService.name, () => {
6969
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
7070
'{{y}}/{{MM}}/{{filename}}',
7171
'{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
72+
'{{#if album}}{{album-startDate-y}}/{{album}}{{else}}{{y}}/Other/{{MM}}{{/if}}/{{filename}}',
7273
'{{y}}/{{MMM}}/{{filename}}',
7374
'{{y}}/{{MMMM}}/{{filename}}',
7475
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
@@ -182,6 +183,63 @@ describe(StorageTemplateService.name, () => {
182183
});
183184
});
184185

186+
it('should handle album startDate', async () => {
187+
const asset = assetStub.storageAsset();
188+
const user = userStub.user1;
189+
const album = albumStub.oneAsset;
190+
const config = structuredClone(defaults);
191+
config.storageTemplate.template =
192+
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
193+
194+
sut.onConfigInit({ newConfig: config });
195+
196+
mocks.user.get.mockResolvedValue(user);
197+
mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset);
198+
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
199+
mocks.album.getMetadataForIds.mockResolvedValueOnce([
200+
{
201+
startDate: asset.fileCreatedAt,
202+
endDate: asset.fileCreatedAt,
203+
albumId: album.id,
204+
assetCount: 1,
205+
lastModifiedAssetTimestamp: null,
206+
},
207+
]);
208+
209+
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
210+
211+
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
212+
expect(mocks.move.create).toHaveBeenCalledWith({
213+
entityId: asset.id,
214+
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`,
215+
oldPath: asset.originalPath,
216+
pathType: AssetPathType.ORIGINAL,
217+
});
218+
});
219+
220+
it('should handle else condition from album startDate', async () => {
221+
const asset = assetStub.storageAsset();
222+
const user = userStub.user1;
223+
const config = structuredClone(defaults);
224+
config.storageTemplate.template =
225+
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
226+
227+
sut.onConfigInit({ newConfig: config });
228+
229+
mocks.user.get.mockResolvedValue(user);
230+
mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset);
231+
232+
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
233+
234+
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
235+
expect(mocks.move.create).toHaveBeenCalledWith({
236+
entityId: asset.id,
237+
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`,
238+
oldPath: asset.originalPath,
239+
pathType: AssetPathType.ORIGINAL,
240+
});
241+
});
242+
185243
it('should migrate previously failed move from original path when it still exists', async () => {
186244
mocks.user.get.mockResolvedValue(userStub.user1);
187245

server/src/services/storage-template.service.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const storagePresets = [
2828
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
2929
'{{y}}/{{MM}}/{{filename}}',
3030
'{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
31+
'{{#if album}}{{album-startDate-y}}/{{album}}{{else}}{{y}}/Other/{{MM}}{{/if}}/{{filename}}',
3132
'{{y}}/{{MMM}}/{{filename}}',
3233
'{{y}}/{{MMMM}}/{{filename}}',
3334
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
@@ -54,6 +55,8 @@ interface RenderMetadata {
5455
filename: string;
5556
extension: string;
5657
albumName: string | null;
58+
albumStartDate: Date | null;
59+
albumEndDate: Date | null;
5760
}
5861

5962
@Injectable()
@@ -62,6 +65,7 @@ export class StorageTemplateService extends BaseService {
6265
compiled: HandlebarsTemplateDelegate<any>;
6366
raw: string;
6467
needsAlbum: boolean;
68+
needsAlbumMetadata: boolean;
6569
} | null = null;
6670

6771
private get template() {
@@ -99,6 +103,8 @@ export class StorageTemplateService extends BaseService {
99103
filename: 'IMG_123',
100104
extension: 'jpg',
101105
albumName: 'album',
106+
albumStartDate: new Date(),
107+
albumEndDate: new Date(),
102108
});
103109
} catch (error) {
104110
this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`);
@@ -255,16 +261,29 @@ export class StorageTemplateService extends BaseService {
255261
}
256262

257263
let albumName = null;
264+
let albumStartDate = null;
265+
let albumEndDate = null;
258266
if (this.template.needsAlbum) {
259267
const albums = await this.albumRepository.getByAssetId(asset.ownerId, asset.id);
260-
albumName = albums?.[0]?.albumName || null;
268+
const album = albums?.[0];
269+
if (album) {
270+
albumName = album.albumName || null;
271+
272+
if (this.template.needsAlbumMetadata) {
273+
const [metadata] = await this.albumRepository.getMetadataForIds([album.id]);
274+
albumStartDate = metadata?.startDate || null;
275+
albumEndDate = metadata?.endDate || null;
276+
}
277+
}
261278
}
262279

263280
const storagePath = this.render(this.template.compiled, {
264281
asset,
265282
filename: sanitized,
266283
extension,
267284
albumName,
285+
albumStartDate,
286+
albumEndDate,
268287
});
269288
const fullPath = path.normalize(path.join(rootPath, storagePath));
270289
let destination = `${fullPath}.${extension}`;
@@ -323,12 +342,13 @@ export class StorageTemplateService extends BaseService {
323342
return {
324343
raw: template,
325344
compiled: handlebar.compile(template, { knownHelpers: undefined, strict: true }),
326-
needsAlbum: template.includes('{{album}}'),
345+
needsAlbum: template.includes('album'),
346+
needsAlbumMetadata: template.includes('album-startDate') || template.includes('album-endDate'),
327347
};
328348
}
329349

330350
private render(template: HandlebarsTemplateDelegate<any>, options: RenderMetadata) {
331-
const { filename, extension, asset, albumName } = options;
351+
const { filename, extension, asset, albumName, albumStartDate, albumEndDate } = options;
332352
const substitutions: Record<string, string> = {
333353
filename,
334354
ext: extension,
@@ -346,6 +366,15 @@ export class StorageTemplateService extends BaseService {
346366

347367
for (const token of Object.values(storageTokens).flat()) {
348368
substitutions[token] = dt.toFormat(token);
369+
if (albumName) {
370+
// Use system time zone for album dates to ensure all assets get the exact same date.
371+
substitutions['album-startDate-' + token] = albumStartDate
372+
? DateTime.fromJSDate(albumStartDate, { zone: systemTimeZone }).toFormat(token)
373+
: '';
374+
substitutions['album-endDate-' + token] = albumEndDate
375+
? DateTime.fromJSDate(albumEndDate, { zone: systemTimeZone }).toFormat(token)
376+
: '';
377+
}
349378
}
350379

351380
return template(substitutions).replaceAll(/\/{2,}/gm, '/');

web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
};
7979
8080
const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString());
81+
const albumStartTime = luxon.DateTime.fromISO(new Date('2021-12-31T05:32:41.750').toISOString());
82+
const albumEndTime = luxon.DateTime.fromISO(new Date('2023-05-06T09:15:17.100').toISOString());
8183
8284
const dateTokens = [
8385
...templateOptions.yearOptions,
@@ -91,6 +93,8 @@
9193
9294
for (const token of dateTokens) {
9395
substitutions[token] = dt.toFormat(token);
96+
substitutions['album-startDate-' + token] = albumStartTime.toFormat(token);
97+
substitutions['album-endDate-' + token] = albumEndTime.toFormat(token);
9498
}
9599
96100
return template(substitutions);

web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
<li>{`{{assetId}}`} - Asset ID</li>
3030
<li>{`{{assetIdShort}}`} - Asset ID (last 12 characters)</li>
3131
<li>{`{{album}}`} - Album Name</li>
32+
<li>
33+
{`{{album-startDate-x}}`} - Album Start Date and Time (e.g. album-startDate-yy).
34+
{$t('admin.storage_template_date_time_sample', { values: { date: '2021-12-31T05:32:41.750' } })}
35+
</li>
36+
<li>
37+
{`{{album-endDate-x}}`} - Album End Date and Time (e.g. album-endDate-MM).
38+
{$t('admin.storage_template_date_time_sample', { values: { date: '2023-05-06T09:15:17.100' } })}
39+
</li>
3240
</ul>
3341
</div>
3442
</div>

0 commit comments

Comments
 (0)