includio-cms 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/media-gc.d.ts +3 -0
- package/dist/admin/api/media-gc.js +27 -0
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +205 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte.d.ts +3 -0
- package/dist/admin/components/layout/lang.d.ts +1 -0
- package/dist/admin/components/layout/lang.js +4 -2
- package/dist/admin/components/layout/nav-main.svelte +6 -0
- package/dist/core/server/media/operations/purgeImageStyles.d.ts +7 -0
- package/dist/core/server/media/operations/purgeImageStyles.js +25 -0
- package/dist/core/server/media/operations/reconcileMedia.d.ts +12 -0
- package/dist/core/server/media/operations/reconcileMedia.js +62 -0
- package/dist/core/server/media/styles/operations/createMediaStyle.js +12 -1
- package/dist/db-postgres/index.js +25 -12
- package/dist/db-postgres/schema/imageStyle.js +2 -0
- package/dist/files-local/index.js +11 -1
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/types/adapters/db.d.ts +8 -0
- package/dist/types/adapters/files.d.ts +2 -0
- package/dist/updates/0.5.8/index.d.ts +2 -0
- package/dist/updates/0.5.8/index.js +27 -0
- package/dist/updates/index.js +2 -1
- package/package.json +34 -14
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.5.8 — 2026-03-09
|
|
7
|
+
|
|
8
|
+
Media GC: deduplicate image styles, auto-cleanup, admin maintenance page
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Image style upsert: replace duplicates instead of creating new files
|
|
12
|
+
- Auto-cleanup old style files from disk on regeneration
|
|
13
|
+
- Admin maintenance page: purge styles, reconcile orphaned disk files
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Deduplicate existing image style records (keep newest per unique key)
|
|
17
|
+
|
|
18
|
+
### Migration
|
|
19
|
+
|
|
20
|
+
```sql
|
|
21
|
+
-- Deduplicate: keep newest style (by id) per unique key, delete rest
|
|
22
|
+
DELETE FROM image_styles a
|
|
23
|
+
USING image_styles b
|
|
24
|
+
WHERE a.media_file_id = b.media_file_id
|
|
25
|
+
AND a.name = b.name
|
|
26
|
+
AND COALESCE(a.width, 0) = COALESCE(b.width, 0)
|
|
27
|
+
AND COALESCE(a.height, 0) = COALESCE(b.height, 0)
|
|
28
|
+
AND COALESCE(a.quality, 0) = COALESCE(b.quality, 0)
|
|
29
|
+
AND a.id < b.id;
|
|
30
|
+
|
|
31
|
+
-- Unique index to prevent future duplicates
|
|
32
|
+
CREATE UNIQUE INDEX IF NOT EXISTS image_styles_unique_key
|
|
33
|
+
ON image_styles (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0));
|
|
34
|
+
```
|
|
35
|
+
|
|
6
36
|
## 0.5.7 — 2026-03-06
|
|
7
37
|
|
|
8
38
|
SEO field with slug toggle, structured-content & URL field fixes
|
|
@@ -4,6 +4,7 @@ import * as orphanedHandlers from './orphaned.js';
|
|
|
4
4
|
import * as replaceHandlers from './replace.js';
|
|
5
5
|
import * as inviteHandlers from './invite.js';
|
|
6
6
|
import * as acceptInviteHandlers from './accept-invite.js';
|
|
7
|
+
import * as mediaGcHandlers from './media-gc.js';
|
|
7
8
|
export function createAdminApiHandler(options) {
|
|
8
9
|
const routes = {
|
|
9
10
|
upload: uploadHandlers,
|
|
@@ -11,6 +12,7 @@ export function createAdminApiHandler(options) {
|
|
|
11
12
|
replace: replaceHandlers,
|
|
12
13
|
invite: inviteHandlers,
|
|
13
14
|
'accept-invite': acceptInviteHandlers,
|
|
15
|
+
'media-gc': mediaGcHandlers,
|
|
14
16
|
...options?.extraRoutes
|
|
15
17
|
};
|
|
16
18
|
function handle(method) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { requireRole } from '../remote/middleware/auth.js';
|
|
2
|
+
import { getImageStylesStats, purgeAllImageStyles } from '../../core/server/media/operations/purgeImageStyles.js';
|
|
3
|
+
import { getReconciliationReport, deleteOrphanedDiskFiles } from '../../core/server/media/operations/reconcileMedia.js';
|
|
4
|
+
import { json } from '@sveltejs/kit';
|
|
5
|
+
export const GET = async ({ url }) => {
|
|
6
|
+
requireRole('admin');
|
|
7
|
+
const stats = await getImageStylesStats();
|
|
8
|
+
const report = await getReconciliationReport();
|
|
9
|
+
return json({
|
|
10
|
+
imageStylesCount: stats.count,
|
|
11
|
+
orphanedDiskFiles: report.orphanedDisk,
|
|
12
|
+
missingDiskRecords: report.missingDisk
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
export const DELETE = async ({ url }) => {
|
|
16
|
+
requireRole('admin');
|
|
17
|
+
const action = url.searchParams.get('action');
|
|
18
|
+
if (action === 'purge-styles') {
|
|
19
|
+
const result = await purgeAllImageStyles();
|
|
20
|
+
return json(result);
|
|
21
|
+
}
|
|
22
|
+
if (action === 'delete-orphaned-disk') {
|
|
23
|
+
const result = await deleteOrphanedDiskFiles();
|
|
24
|
+
return json(result);
|
|
25
|
+
}
|
|
26
|
+
return json({ error: 'Unknown action' }, { status: 400 });
|
|
27
|
+
};
|
|
@@ -9,3 +9,4 @@ export { default as FormPage } from './form/form-page.svelte';
|
|
|
9
9
|
export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
|
|
10
10
|
export { default as UsersPage } from './users/users-page.svelte';
|
|
11
11
|
export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
|
|
12
|
+
export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
|
|
@@ -9,3 +9,4 @@ export { default as FormPage } from './form/form-page.svelte';
|
|
|
9
9
|
export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
|
|
10
10
|
export { default as UsersPage } from './users/users-page.svelte';
|
|
11
11
|
export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
|
|
12
|
+
export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Loader2 from '@tabler/icons-svelte/icons/loader-2';
|
|
3
|
+
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
4
|
+
import Refresh from '@tabler/icons-svelte/icons/refresh';
|
|
5
|
+
import FileSearch from '@tabler/icons-svelte/icons/file-search';
|
|
6
|
+
import Photo from '@tabler/icons-svelte/icons/photo';
|
|
7
|
+
import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
|
|
8
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
9
|
+
import * as Card from '../../../components/ui/card/index.js';
|
|
10
|
+
import { toast } from 'svelte-sonner';
|
|
11
|
+
|
|
12
|
+
interface GcReport {
|
|
13
|
+
imageStylesCount: number;
|
|
14
|
+
orphanedDiskFiles: string[];
|
|
15
|
+
missingDiskRecords: { table: string; id: string; url: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let report = $state<GcReport | null>(null);
|
|
19
|
+
let loading = $state(true);
|
|
20
|
+
let purging = $state(false);
|
|
21
|
+
let cleaningOrphans = $state(false);
|
|
22
|
+
|
|
23
|
+
async function loadReport() {
|
|
24
|
+
loading = true;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch('/admin/api/media-gc');
|
|
27
|
+
if (!res.ok) throw new Error('Failed to load');
|
|
28
|
+
report = await res.json();
|
|
29
|
+
} catch {
|
|
30
|
+
toast.error('Nie udało się załadować raportu');
|
|
31
|
+
} finally {
|
|
32
|
+
loading = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function purgeStyles() {
|
|
37
|
+
purging = true;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch('/admin/api/media-gc?action=purge-styles', { method: 'DELETE' });
|
|
40
|
+
if (!res.ok) throw new Error('Failed');
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
toast.success(`Usunięto ${data.deletedCount} styli (${data.filesDeleted} plików z dysku)`);
|
|
43
|
+
await loadReport();
|
|
44
|
+
} catch {
|
|
45
|
+
toast.error('Nie udało się usunąć styli');
|
|
46
|
+
} finally {
|
|
47
|
+
purging = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function cleanOrphans() {
|
|
52
|
+
cleaningOrphans = true;
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch('/admin/api/media-gc?action=delete-orphaned-disk', { method: 'DELETE' });
|
|
55
|
+
if (!res.ok) throw new Error('Failed');
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
toast.success(`Usunięto ${data.deletedCount} osieroconych plików`);
|
|
58
|
+
await loadReport();
|
|
59
|
+
} catch {
|
|
60
|
+
toast.error('Nie udało się wyczyścić plików');
|
|
61
|
+
} finally {
|
|
62
|
+
cleaningOrphans = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$effect(() => {
|
|
67
|
+
loadReport();
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div class="p-5 pb-24 md:p-7">
|
|
72
|
+
<div class="mb-6">
|
|
73
|
+
<h1 class="mb-1 text-2xl font-bold">Konserwacja</h1>
|
|
74
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
75
|
+
Narzędzia do zarządzania plikami mediów i stylami obrazów
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{#if loading}
|
|
80
|
+
<div class="flex items-center justify-center py-20">
|
|
81
|
+
<Loader2 class="size-6 animate-spin" style="color: var(--muted-foreground);" />
|
|
82
|
+
</div>
|
|
83
|
+
{:else if report}
|
|
84
|
+
<div class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
|
85
|
+
<!-- Image styles -->
|
|
86
|
+
<Card.Root>
|
|
87
|
+
<Card.Header>
|
|
88
|
+
<div class="flex items-center gap-2">
|
|
89
|
+
<Photo class="size-5" style="color: var(--primary);" />
|
|
90
|
+
<Card.Title>Style obrazów</Card.Title>
|
|
91
|
+
</div>
|
|
92
|
+
<Card.Description>
|
|
93
|
+
Wygenerowane warianty (mniejsze rozmiary, WebP/AVIF)
|
|
94
|
+
</Card.Description>
|
|
95
|
+
</Card.Header>
|
|
96
|
+
<Card.Content>
|
|
97
|
+
<p class="mb-4 text-3xl font-bold" style="color: var(--primary);">
|
|
98
|
+
{report.imageStylesCount}
|
|
99
|
+
</p>
|
|
100
|
+
<Button
|
|
101
|
+
variant="destructive"
|
|
102
|
+
size="sm"
|
|
103
|
+
onclick={purgeStyles}
|
|
104
|
+
disabled={purging || report.imageStylesCount === 0}
|
|
105
|
+
>
|
|
106
|
+
{#if purging}
|
|
107
|
+
<Loader2 class="size-4 animate-spin" />
|
|
108
|
+
{:else}
|
|
109
|
+
<Trash class="size-4" />
|
|
110
|
+
{/if}
|
|
111
|
+
Usuń wszystkie i regeneruj
|
|
112
|
+
</Button>
|
|
113
|
+
<p class="mt-2 text-xs" style="color: var(--text-light);">
|
|
114
|
+
Style zostaną odtworzone automatycznie przy kolejnym wyświetleniu
|
|
115
|
+
</p>
|
|
116
|
+
</Card.Content>
|
|
117
|
+
</Card.Root>
|
|
118
|
+
|
|
119
|
+
<!-- Orphaned disk files -->
|
|
120
|
+
<Card.Root>
|
|
121
|
+
<Card.Header>
|
|
122
|
+
<div class="flex items-center gap-2">
|
|
123
|
+
<FileSearch class="size-5" style="color: var(--warning, #C4893A);" />
|
|
124
|
+
<Card.Title>Osierocone pliki</Card.Title>
|
|
125
|
+
</div>
|
|
126
|
+
<Card.Description>
|
|
127
|
+
Pliki na dysku bez odpowiadających rekordów w bazie danych
|
|
128
|
+
</Card.Description>
|
|
129
|
+
</Card.Header>
|
|
130
|
+
<Card.Content>
|
|
131
|
+
<p class="mb-4 text-3xl font-bold" style="color: {report.orphanedDiskFiles.length > 0 ? 'var(--warning, #C4893A)' : 'var(--success, #3A8A5C)'};">
|
|
132
|
+
{report.orphanedDiskFiles.length}
|
|
133
|
+
</p>
|
|
134
|
+
{#if report.orphanedDiskFiles.length > 0}
|
|
135
|
+
<Button
|
|
136
|
+
variant="outline"
|
|
137
|
+
size="sm"
|
|
138
|
+
onclick={cleanOrphans}
|
|
139
|
+
disabled={cleaningOrphans}
|
|
140
|
+
>
|
|
141
|
+
{#if cleaningOrphans}
|
|
142
|
+
<Loader2 class="size-4 animate-spin" />
|
|
143
|
+
{:else}
|
|
144
|
+
<Trash class="size-4" />
|
|
145
|
+
{/if}
|
|
146
|
+
Usuń osierocone pliki
|
|
147
|
+
</Button>
|
|
148
|
+
<details class="mt-3">
|
|
149
|
+
<summary class="cursor-pointer text-xs" style="color: var(--text-light);">
|
|
150
|
+
Pokaż pliki ({report.orphanedDiskFiles.length})
|
|
151
|
+
</summary>
|
|
152
|
+
<ul class="mt-1 max-h-40 overflow-auto text-xs" style="color: var(--muted-foreground);">
|
|
153
|
+
{#each report.orphanedDiskFiles as file}
|
|
154
|
+
<li class="truncate py-0.5">{file}</li>
|
|
155
|
+
{/each}
|
|
156
|
+
</ul>
|
|
157
|
+
</details>
|
|
158
|
+
{:else}
|
|
159
|
+
<p class="text-sm" style="color: var(--success, #3A8A5C);">Wszystko w porządku</p>
|
|
160
|
+
{/if}
|
|
161
|
+
</Card.Content>
|
|
162
|
+
</Card.Root>
|
|
163
|
+
|
|
164
|
+
<!-- Missing disk files -->
|
|
165
|
+
<Card.Root>
|
|
166
|
+
<Card.Header>
|
|
167
|
+
<div class="flex items-center gap-2">
|
|
168
|
+
<AlertTriangle class="size-5" style="color: var(--error, #C44B4B);" />
|
|
169
|
+
<Card.Title>Brakujące pliki</Card.Title>
|
|
170
|
+
</div>
|
|
171
|
+
<Card.Description>
|
|
172
|
+
Rekordy w bazie danych bez plików na dysku
|
|
173
|
+
</Card.Description>
|
|
174
|
+
</Card.Header>
|
|
175
|
+
<Card.Content>
|
|
176
|
+
<p class="mb-4 text-3xl font-bold" style="color: {report.missingDiskRecords.length > 0 ? 'var(--error, #C44B4B)' : 'var(--success, #3A8A5C)'};">
|
|
177
|
+
{report.missingDiskRecords.length}
|
|
178
|
+
</p>
|
|
179
|
+
{#if report.missingDiskRecords.length > 0}
|
|
180
|
+
<details>
|
|
181
|
+
<summary class="cursor-pointer text-xs" style="color: var(--text-light);">
|
|
182
|
+
Pokaż rekordy ({report.missingDiskRecords.length})
|
|
183
|
+
</summary>
|
|
184
|
+
<ul class="mt-1 max-h-40 overflow-auto text-xs" style="color: var(--muted-foreground);">
|
|
185
|
+
{#each report.missingDiskRecords as rec}
|
|
186
|
+
<li class="truncate py-0.5">{rec.table}: {rec.url}</li>
|
|
187
|
+
{/each}
|
|
188
|
+
</ul>
|
|
189
|
+
</details>
|
|
190
|
+
{:else}
|
|
191
|
+
<p class="text-sm" style="color: var(--success, #3A8A5C);">Wszystko w porządku</p>
|
|
192
|
+
{/if}
|
|
193
|
+
</Card.Content>
|
|
194
|
+
</Card.Root>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Refresh button -->
|
|
198
|
+
<div class="mt-6">
|
|
199
|
+
<Button variant="outline" size="sm" onclick={loadReport} disabled={loading}>
|
|
200
|
+
<Refresh class="size-4" />
|
|
201
|
+
Odśwież raport
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
@@ -8,7 +8,8 @@ export const sidebarLang = {
|
|
|
8
8
|
platform: 'Platforma',
|
|
9
9
|
media: 'Biblioteka mediów',
|
|
10
10
|
dashboard: 'Pulpit',
|
|
11
|
-
users: 'Użytkownicy'
|
|
11
|
+
users: 'Użytkownicy',
|
|
12
|
+
maintenance: 'Konserwacja'
|
|
12
13
|
},
|
|
13
14
|
singletons: {
|
|
14
15
|
title: 'Pojedyncze'
|
|
@@ -40,7 +41,8 @@ export const sidebarLang = {
|
|
|
40
41
|
platform: 'Platform',
|
|
41
42
|
media: 'Media Library',
|
|
42
43
|
dashboard: 'Dashboard',
|
|
43
|
-
users: 'Users'
|
|
44
|
+
users: 'Users',
|
|
45
|
+
maintenance: 'Maintenance'
|
|
44
46
|
},
|
|
45
47
|
singletons: {
|
|
46
48
|
title: 'Singletons'
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import CameraIcon from '@tabler/icons-svelte/icons/camera';
|
|
7
7
|
import LayoutDashboardIcon from '@tabler/icons-svelte/icons/layout-dashboard';
|
|
8
8
|
import UsersIcon from '@tabler/icons-svelte/icons/users';
|
|
9
|
+
import SettingsIcon from '@tabler/icons-svelte/icons/settings';
|
|
9
10
|
import { page } from '$app/state';
|
|
10
11
|
import { authClient } from '../../auth-client.js';
|
|
11
12
|
import { resolve } from '$app/paths';
|
|
@@ -33,6 +34,11 @@
|
|
|
33
34
|
title: sidebarLang[interfaceLanguage.current].main.users,
|
|
34
35
|
url: '/admin/users',
|
|
35
36
|
icon: UsersIcon
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: sidebarLang[interfaceLanguage.current].main.maintenance,
|
|
40
|
+
url: '/admin/maintenance',
|
|
41
|
+
icon: SettingsIcon
|
|
36
42
|
}
|
|
37
43
|
]
|
|
38
44
|
: [])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getCMS } from '../../../cms.js';
|
|
2
|
+
export async function getImageStylesStats() {
|
|
3
|
+
const styles = await getCMS().databaseAdapter.getAllImageStyles();
|
|
4
|
+
return { count: styles.length };
|
|
5
|
+
}
|
|
6
|
+
export async function purgeAllImageStyles() {
|
|
7
|
+
const cms = getCMS();
|
|
8
|
+
const styles = await cms.databaseAdapter.getAllImageStyles();
|
|
9
|
+
// Delete files from disk
|
|
10
|
+
let filesDeleted = 0;
|
|
11
|
+
for (const style of styles) {
|
|
12
|
+
const filename = style.url.split('/').pop();
|
|
13
|
+
if (filename) {
|
|
14
|
+
try {
|
|
15
|
+
await cms.filesAdapter.deleteFile(filename);
|
|
16
|
+
filesDeleted++;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// File may not exist
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const deletedCount = await cms.databaseAdapter.deleteAllImageStyles();
|
|
24
|
+
return { deletedCount, filesDeleted };
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ReconciliationReport {
|
|
2
|
+
orphanedDisk: string[];
|
|
3
|
+
missingDisk: {
|
|
4
|
+
table: string;
|
|
5
|
+
id: string;
|
|
6
|
+
url: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
export declare function getReconciliationReport(): Promise<ReconciliationReport>;
|
|
10
|
+
export declare function deleteOrphanedDiskFiles(): Promise<{
|
|
11
|
+
deletedCount: number;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getCMS } from '../../../cms.js';
|
|
2
|
+
export async function getReconciliationReport() {
|
|
3
|
+
const cms = getCMS();
|
|
4
|
+
const diskFiles = await cms.filesAdapter.listFiles();
|
|
5
|
+
// Collect all known URLs from DB
|
|
6
|
+
const mediaFiles = await cms.databaseAdapter.getMediaFiles({ data: {} });
|
|
7
|
+
const imageStyles = await cms.databaseAdapter.getAllImageStyles();
|
|
8
|
+
const dbFilenames = new Set();
|
|
9
|
+
for (const mf of mediaFiles) {
|
|
10
|
+
const fn = mf.url.split('/').pop();
|
|
11
|
+
if (fn)
|
|
12
|
+
dbFilenames.add(fn);
|
|
13
|
+
if (mf.thumbnailUrl) {
|
|
14
|
+
const tfn = mf.thumbnailUrl.split('/').pop();
|
|
15
|
+
if (tfn)
|
|
16
|
+
dbFilenames.add(tfn);
|
|
17
|
+
}
|
|
18
|
+
if (mf.posterUrl) {
|
|
19
|
+
const pfn = mf.posterUrl.split('/').pop();
|
|
20
|
+
if (pfn)
|
|
21
|
+
dbFilenames.add(pfn);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const is of imageStyles) {
|
|
25
|
+
const fn = is.url.split('/').pop();
|
|
26
|
+
if (fn)
|
|
27
|
+
dbFilenames.add(fn);
|
|
28
|
+
}
|
|
29
|
+
// Orphaned: on disk but not in DB
|
|
30
|
+
const orphanedDisk = diskFiles.filter((f) => !dbFilenames.has(f));
|
|
31
|
+
// Missing: in DB but not on disk
|
|
32
|
+
const diskSet = new Set(diskFiles);
|
|
33
|
+
const missingDisk = [];
|
|
34
|
+
for (const mf of mediaFiles) {
|
|
35
|
+
const fn = mf.url.split('/').pop();
|
|
36
|
+
if (fn && !diskSet.has(fn)) {
|
|
37
|
+
missingDisk.push({ table: 'media_files', id: mf.id, url: mf.url });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const is of imageStyles) {
|
|
41
|
+
const fn = is.url.split('/').pop();
|
|
42
|
+
if (fn && !diskSet.has(fn)) {
|
|
43
|
+
missingDisk.push({ table: 'image_styles', id: is.id, url: is.url });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { orphanedDisk, missingDisk };
|
|
47
|
+
}
|
|
48
|
+
export async function deleteOrphanedDiskFiles() {
|
|
49
|
+
const cms = getCMS();
|
|
50
|
+
const report = await getReconciliationReport();
|
|
51
|
+
let deletedCount = 0;
|
|
52
|
+
for (const filename of report.orphanedDisk) {
|
|
53
|
+
try {
|
|
54
|
+
await cms.filesAdapter.deleteFile(filename);
|
|
55
|
+
deletedCount++;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { deletedCount };
|
|
62
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { getCMS } from '../../../../cms.js';
|
|
2
2
|
import { generateImageStyle } from '../sharp/generateImageStyle.js';
|
|
3
3
|
export async function createImageStyle(mediaFileId, style) {
|
|
4
|
+
const cms = getCMS();
|
|
5
|
+
// Check for existing style to clean up old file after upsert
|
|
6
|
+
const existing = await cms.databaseAdapter.getImageStyle(mediaFileId, style);
|
|
7
|
+
const oldUrl = existing?.url;
|
|
4
8
|
const imageStyleFile = await generateImageStyle(mediaFileId, style);
|
|
5
|
-
const imageStyle = await
|
|
9
|
+
const imageStyle = await cms.databaseAdapter.createImageStyle(mediaFileId, imageStyleFile, style);
|
|
10
|
+
// Delete old file from disk if URL changed
|
|
11
|
+
if (oldUrl && oldUrl !== imageStyle.url) {
|
|
12
|
+
const oldFilename = oldUrl.split('/').pop();
|
|
13
|
+
if (oldFilename) {
|
|
14
|
+
cms.filesAdapter.deleteFile(oldFilename).catch((e) => console.warn('Old style file cleanup failed:', e));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
return imageStyle;
|
|
7
18
|
}
|
|
@@ -326,19 +326,19 @@ export function pg(config) {
|
|
|
326
326
|
};
|
|
327
327
|
},
|
|
328
328
|
createImageStyle: async (mediaFileId, file, style) => {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
const mimeType = file.mimeType ?? `image/${style.format}`;
|
|
330
|
+
const rows = await db.execute(sql `
|
|
331
|
+
INSERT INTO image_styles (id, media_file_id, name, url, width, height, crop, quality, media, mime_type)
|
|
332
|
+
VALUES (gen_random_uuid(), ${mediaFileId}, ${style.name}, ${file.url}, ${style.width ?? null}, ${style.height ?? null}, ${style.crop ?? false}, ${style.quality ?? null}, ${style.media ?? null}, ${mimeType})
|
|
333
|
+
ON CONFLICT (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0))
|
|
334
|
+
DO UPDATE SET url = EXCLUDED.url, mime_type = EXCLUDED.mime_type
|
|
335
|
+
RETURNING url, mime_type, media
|
|
336
|
+
`);
|
|
337
|
+
const row = rows[0];
|
|
338
338
|
return {
|
|
339
|
-
url:
|
|
340
|
-
mimeType:
|
|
341
|
-
media:
|
|
339
|
+
url: row.url,
|
|
340
|
+
mimeType: row.mime_type,
|
|
341
|
+
media: row.media ? row.media : undefined
|
|
342
342
|
};
|
|
343
343
|
},
|
|
344
344
|
deleteMediaFile: async (id) => {
|
|
@@ -421,6 +421,19 @@ export function pg(config) {
|
|
|
421
421
|
.returning();
|
|
422
422
|
const [file] = await hydrateFileTags(db, [rawFile]);
|
|
423
423
|
return file;
|
|
424
|
+
},
|
|
425
|
+
getAllImageStyles: async () => {
|
|
426
|
+
return db
|
|
427
|
+
.select({
|
|
428
|
+
id: schema.imageStylesTable.id,
|
|
429
|
+
url: schema.imageStylesTable.url,
|
|
430
|
+
mediaFileId: schema.imageStylesTable.mediaFileId
|
|
431
|
+
})
|
|
432
|
+
.from(schema.imageStylesTable);
|
|
433
|
+
},
|
|
434
|
+
deleteAllImageStyles: async () => {
|
|
435
|
+
const result = await db.execute(sql `DELETE FROM image_styles`);
|
|
436
|
+
return Number(result.count ?? 0);
|
|
424
437
|
}
|
|
425
438
|
};
|
|
426
439
|
}
|
|
@@ -11,3 +11,5 @@ export const imageStylesTable = pgTable('image_styles', {
|
|
|
11
11
|
media: text('media'),
|
|
12
12
|
mimeType: text('mime_type').notNull()
|
|
13
13
|
});
|
|
14
|
+
// NOTE: unique index on (media_file_id, name, COALESCE(width,0), COALESCE(height,0), COALESCE(quality,0))
|
|
15
|
+
// is managed via SQL migration in src/lib/updates/0.5.8/index.ts (expression-based, not supported by Drizzle ORM)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, extname, resolve } from 'node:path';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { readFile, writeFile, rename, unlink, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { readFile, writeFile, rename, unlink, mkdir, readdir } from 'node:fs/promises';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import ffmpeg from 'fluent-ffmpeg';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
@@ -301,6 +301,16 @@ export function local() {
|
|
|
301
301
|
catch {
|
|
302
302
|
// File may already be deleted
|
|
303
303
|
}
|
|
304
|
+
},
|
|
305
|
+
listFiles: async () => {
|
|
306
|
+
try {
|
|
307
|
+
await ensureDir(fullDir);
|
|
308
|
+
const entries = await readdir(fullDir, { withFileTypes: true });
|
|
309
|
+
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
304
314
|
}
|
|
305
315
|
};
|
|
306
316
|
}
|
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
locale?: "en" | "pl";
|
|
5
|
-
}): string;
|
|
6
|
-
/**
|
|
7
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
8
|
-
*
|
|
9
|
-
* - Changing this function will be over-written by the next build.
|
|
10
|
-
*
|
|
11
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
12
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
13
|
-
*
|
|
14
|
-
* @param {{}} inputs
|
|
15
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
16
|
-
* @returns {string}
|
|
17
|
-
*/
|
|
18
|
-
declare function login_hello(inputs?: {}, options?: {
|
|
19
|
-
locale?: "en" | "pl";
|
|
20
|
-
}): string;
|
|
21
|
-
/**
|
|
22
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
23
|
-
*
|
|
24
|
-
* - Changing this function will be over-written by the next build.
|
|
25
|
-
*
|
|
26
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
27
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
28
|
-
*
|
|
29
|
-
* @param {{}} inputs
|
|
30
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
31
|
-
* @returns {string}
|
|
32
|
-
*/
|
|
33
|
-
declare function login_please_login(inputs?: {}, options?: {
|
|
34
|
-
locale?: "en" | "pl";
|
|
35
|
-
}): string;
|
|
36
|
-
export { login_hello as login.hello, login_please_login as login.please_login };
|
|
1
|
+
export * from "./hello_world.js";
|
|
2
|
+
export * from "./login_hello.js";
|
|
3
|
+
export * from "./login_please_login.js";
|
|
@@ -1,72 +1,4 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
7
|
-
*
|
|
8
|
-
* - Changing this function will be over-written by the next build.
|
|
9
|
-
*
|
|
10
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
11
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
12
|
-
*
|
|
13
|
-
* @param {{ name: NonNullable<unknown> }} inputs
|
|
14
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
15
|
-
* @returns {string}
|
|
16
|
-
*/
|
|
17
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
18
|
-
export const hello_world = (inputs, options = {}) => {
|
|
19
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
20
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
21
|
-
}
|
|
22
|
-
const locale = options.locale ?? getLocale()
|
|
23
|
-
trackMessageCall("hello_world", locale)
|
|
24
|
-
if (locale === "en") return en.hello_world(inputs)
|
|
25
|
-
return pl.hello_world(inputs)
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
29
|
-
*
|
|
30
|
-
* - Changing this function will be over-written by the next build.
|
|
31
|
-
*
|
|
32
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
33
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
34
|
-
*
|
|
35
|
-
* @param {{}} inputs
|
|
36
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
37
|
-
* @returns {string}
|
|
38
|
-
*/
|
|
39
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
40
|
-
const login_hello = (inputs = {}, options = {}) => {
|
|
41
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
42
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
43
|
-
}
|
|
44
|
-
const locale = options.locale ?? getLocale()
|
|
45
|
-
trackMessageCall("login_hello", locale)
|
|
46
|
-
if (locale === "en") return en.login_hello(inputs)
|
|
47
|
-
return pl.login_hello(inputs)
|
|
48
|
-
};
|
|
49
|
-
export { login_hello as "login.hello" }
|
|
50
|
-
/**
|
|
51
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
52
|
-
*
|
|
53
|
-
* - Changing this function will be over-written by the next build.
|
|
54
|
-
*
|
|
55
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
56
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
57
|
-
*
|
|
58
|
-
* @param {{}} inputs
|
|
59
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
60
|
-
* @returns {string}
|
|
61
|
-
*/
|
|
62
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
63
|
-
const login_please_login = (inputs = {}, options = {}) => {
|
|
64
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
65
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
66
|
-
}
|
|
67
|
-
const locale = options.locale ?? getLocale()
|
|
68
|
-
trackMessageCall("login_please_login", locale)
|
|
69
|
-
if (locale === "en") return en.login_please_login(inputs)
|
|
70
|
-
return pl.login_please_login(inputs)
|
|
71
|
-
};
|
|
72
|
-
export { login_please_login as "login.please_login" }
|
|
2
|
+
export * from './hello_world.js'
|
|
3
|
+
export * from './login_hello.js'
|
|
4
|
+
export * from './login_please_login.js'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
const en_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
+
return `Hello, ${i.name} from en!`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const pl_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
9
|
+
return `Hello, ${i.name} from pl!`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
+
*
|
|
15
|
+
* - Changing this function will be over-written by the next build.
|
|
16
|
+
*
|
|
17
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
+
*
|
|
20
|
+
* @param {{ name: NonNullable<unknown> }} inputs
|
|
21
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
export const hello_world = (inputs, options = {}) => {
|
|
26
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
28
|
+
}
|
|
29
|
+
const locale = options.locale ?? getLocale()
|
|
30
|
+
trackMessageCall("hello_world", locale)
|
|
31
|
+
if (locale === "en") return en_hello_world(inputs)
|
|
32
|
+
return pl_hello_world(inputs)
|
|
33
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { login_hello as login.hello };
|
|
2
|
+
/**
|
|
3
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
+
*
|
|
5
|
+
* - Changing this function will be over-written by the next build.
|
|
6
|
+
*
|
|
7
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
+
*
|
|
10
|
+
* @param {{}} inputs
|
|
11
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
declare function login_hello(inputs?: {}, options?: {
|
|
15
|
+
locale?: "en" | "pl";
|
|
16
|
+
}): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
const en_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
+
return `Welcome back`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const pl_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Witaj ponownie`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
+
*
|
|
15
|
+
* - Changing this function will be over-written by the next build.
|
|
16
|
+
*
|
|
17
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
+
*
|
|
20
|
+
* @param {{}} inputs
|
|
21
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
const login_hello = (inputs = {}, options = {}) => {
|
|
26
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
28
|
+
}
|
|
29
|
+
const locale = options.locale ?? getLocale()
|
|
30
|
+
trackMessageCall("login_hello", locale)
|
|
31
|
+
if (locale === "en") return en_login_hello(inputs)
|
|
32
|
+
return pl_login_hello(inputs)
|
|
33
|
+
};
|
|
34
|
+
export { login_hello as "login.hello" }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { login_please_login as login.please_login };
|
|
2
|
+
/**
|
|
3
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
+
*
|
|
5
|
+
* - Changing this function will be over-written by the next build.
|
|
6
|
+
*
|
|
7
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
+
*
|
|
10
|
+
* @param {{}} inputs
|
|
11
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
declare function login_please_login(inputs?: {}, options?: {
|
|
15
|
+
locale?: "en" | "pl";
|
|
16
|
+
}): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
const en_login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
+
return `Login to your account`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const pl_login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Zaloguj się na swoje konto`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
+
*
|
|
15
|
+
* - Changing this function will be over-written by the next build.
|
|
16
|
+
*
|
|
17
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
+
*
|
|
20
|
+
* @param {{}} inputs
|
|
21
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
const login_please_login = (inputs = {}, options = {}) => {
|
|
26
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
28
|
+
}
|
|
29
|
+
const locale = options.locale ?? getLocale()
|
|
30
|
+
trackMessageCall("login_please_login", locale)
|
|
31
|
+
if (locale === "en") return en_login_please_login(inputs)
|
|
32
|
+
return pl_login_please_login(inputs)
|
|
33
|
+
};
|
|
34
|
+
export { login_please_login as "login.please_login" }
|
|
@@ -32,6 +32,8 @@ export interface DatabaseAdapter {
|
|
|
32
32
|
getImageStyle: GetImageStyle;
|
|
33
33
|
createImageStyle: CreateImageStyle;
|
|
34
34
|
deleteImageStylesByMediaFileId: DeleteImageStylesByMediaFileId;
|
|
35
|
+
getAllImageStyles: GetAllImageStyles;
|
|
36
|
+
deleteAllImageStyles: DeleteAllImageStyles;
|
|
35
37
|
getMediaFiles: GetMediaFiles;
|
|
36
38
|
getMediaFile: GetMediaFile;
|
|
37
39
|
updateMediaFile: UpdateMediaFile;
|
|
@@ -118,4 +120,10 @@ export type CreateImageStyle = (mediaFileId: string, file: UploadedMediaFile, st
|
|
|
118
120
|
export type DeleteImageStylesByMediaFileId = (mediaFileId: string) => Promise<{
|
|
119
121
|
url: string;
|
|
120
122
|
}[]>;
|
|
123
|
+
export type GetAllImageStyles = () => Promise<{
|
|
124
|
+
id: string;
|
|
125
|
+
url: string;
|
|
126
|
+
mediaFileId: string;
|
|
127
|
+
}[]>;
|
|
128
|
+
export type DeleteAllImageStyles = () => Promise<number>;
|
|
121
129
|
export type GetImageStyle = (mediaFileId: string, style: ImageFieldStyle) => Promise<ImageStyle | null>;
|
|
@@ -4,6 +4,7 @@ export interface FilesAdapter {
|
|
|
4
4
|
uploadFile: UploadFile;
|
|
5
5
|
renameFile: RenameFile;
|
|
6
6
|
deleteFile: DeleteFile;
|
|
7
|
+
listFiles: ListFiles;
|
|
7
8
|
}
|
|
8
9
|
export type DownloadFile = (id: string) => Promise<File | null>;
|
|
9
10
|
export type UploadFile = (file: File) => Promise<UploadedMediaFile>;
|
|
@@ -16,3 +17,4 @@ export type RenameFile = (url: string, oldName: string, newName: string) => Prom
|
|
|
16
17
|
error: 'name-already-exists';
|
|
17
18
|
}>;
|
|
18
19
|
export type DeleteFile = (filename: string) => Promise<void>;
|
|
20
|
+
export type ListFiles = () => Promise<string[]>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.5.8',
|
|
3
|
+
date: '2026-03-09',
|
|
4
|
+
description: 'Media GC: deduplicate image styles, auto-cleanup, admin maintenance page',
|
|
5
|
+
features: [
|
|
6
|
+
'Image style upsert: replace duplicates instead of creating new files',
|
|
7
|
+
'Auto-cleanup old style files from disk on regeneration',
|
|
8
|
+
'Admin maintenance page: purge styles, reconcile orphaned disk files'
|
|
9
|
+
],
|
|
10
|
+
fixes: [
|
|
11
|
+
'Deduplicate existing image style records (keep newest per unique key)'
|
|
12
|
+
],
|
|
13
|
+
breakingChanges: [],
|
|
14
|
+
sql: `-- Deduplicate: keep newest style (by id) per unique key, delete rest
|
|
15
|
+
DELETE FROM image_styles a
|
|
16
|
+
USING image_styles b
|
|
17
|
+
WHERE a.media_file_id = b.media_file_id
|
|
18
|
+
AND a.name = b.name
|
|
19
|
+
AND COALESCE(a.width, 0) = COALESCE(b.width, 0)
|
|
20
|
+
AND COALESCE(a.height, 0) = COALESCE(b.height, 0)
|
|
21
|
+
AND COALESCE(a.quality, 0) = COALESCE(b.quality, 0)
|
|
22
|
+
AND a.id < b.id;
|
|
23
|
+
|
|
24
|
+
-- Unique index to prevent future duplicates
|
|
25
|
+
CREATE UNIQUE INDEX IF NOT EXISTS image_styles_unique_key
|
|
26
|
+
ON image_styles (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0));`
|
|
27
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -19,7 +19,8 @@ import { update as update054 } from './0.5.4/index.js';
|
|
|
19
19
|
import { update as update055 } from './0.5.5/index.js';
|
|
20
20
|
import { update as update056 } from './0.5.6/index.js';
|
|
21
21
|
import { update as update057 } from './0.5.7/index.js';
|
|
22
|
-
|
|
22
|
+
import { update as update058 } from './0.5.8/index.js';
|
|
23
|
+
export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058];
|
|
23
24
|
export const getUpdatesFrom = (fromVersion) => {
|
|
24
25
|
const fromParts = fromVersion.split('.').map(Number);
|
|
25
26
|
return updates.filter((update) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "includio-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -126,10 +126,31 @@
|
|
|
126
126
|
"peerDependencies": {
|
|
127
127
|
"@sveltejs/kit": "^2.48.0",
|
|
128
128
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
129
|
+
"better-auth": "^1.3.34",
|
|
130
|
+
"bits-ui": "^2.11.5",
|
|
131
|
+
"drizzle-orm": "^0.40.0",
|
|
132
|
+
"embla-carousel-svelte": "^8.6.0",
|
|
133
|
+
"formsnap": "^2.0.1",
|
|
134
|
+
"paneforge": "^1.0.2",
|
|
135
|
+
"postgres": "^3.4.5",
|
|
136
|
+
"runed": "^0.35.1",
|
|
137
|
+
"sharp": "^0.34.2",
|
|
129
138
|
"svelte": "^5.43.0",
|
|
139
|
+
"svelte-sonner": "^1.0.5",
|
|
140
|
+
"svelte-tiptap": "^3.0.1",
|
|
141
|
+
"sveltekit-superforms": "^2.28.1",
|
|
142
|
+
"tailwind-merge": "^3.3.1",
|
|
143
|
+
"tailwindcss": "^4.1.13",
|
|
130
144
|
"vite": "^7.2.0",
|
|
131
145
|
"zod": "^4.0.0"
|
|
132
146
|
},
|
|
147
|
+
"peerDependenciesMeta": {
|
|
148
|
+
"runed": { "optional": true },
|
|
149
|
+
"svelte-tiptap": { "optional": true },
|
|
150
|
+
"paneforge": { "optional": true },
|
|
151
|
+
"svelte-sonner": { "optional": true },
|
|
152
|
+
"embla-carousel-svelte": { "optional": true }
|
|
153
|
+
},
|
|
133
154
|
"devDependencies": {
|
|
134
155
|
"@chromatic-com/storybook": "^5.0.1",
|
|
135
156
|
"@eslint/compat": "^1.2.5",
|
|
@@ -153,6 +174,8 @@
|
|
|
153
174
|
"@types/fluent-ffmpeg": "^2.1.27",
|
|
154
175
|
"@types/node": "^22",
|
|
155
176
|
"@types/nodemailer": "^7.0.4",
|
|
177
|
+
"better-auth": "^1.3.34",
|
|
178
|
+
"bits-ui": "^2.11.5",
|
|
156
179
|
"clsx": "^2.1.1",
|
|
157
180
|
"drizzle-kit": "^0.30.2",
|
|
158
181
|
"drizzle-orm": "^0.40.0",
|
|
@@ -164,22 +187,31 @@
|
|
|
164
187
|
"flexsearch": "^0.8.212",
|
|
165
188
|
"formsnap": "^2.0.1",
|
|
166
189
|
"globals": "^16.0.0",
|
|
190
|
+
"paneforge": "^1.0.2",
|
|
191
|
+
"postgres": "^3.4.5",
|
|
167
192
|
"jsdom": "^26.0.0",
|
|
168
193
|
"mdsvex": "^0.12.3",
|
|
169
194
|
"prettier": "^3.4.2",
|
|
170
195
|
"prettier-plugin-svelte": "^3.3.3",
|
|
171
196
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
|
172
197
|
"publint": "^0.3.2",
|
|
198
|
+
"runed": "^0.35.1",
|
|
173
199
|
"rehype-autolink-headings": "^7.1.0",
|
|
174
200
|
"rehype-slug": "^6.0.0",
|
|
175
201
|
"remark-gfm": "^4.0.1",
|
|
202
|
+
"sharp": "^0.34.2",
|
|
176
203
|
"shiki": "^3.21.0",
|
|
177
204
|
"storybook": "^10.2.10",
|
|
178
205
|
"svelte": "^5.43.6",
|
|
179
206
|
"svelte-check": "^4.0.0",
|
|
207
|
+
"svelte-sonner": "^1.0.5",
|
|
208
|
+
"svelte-tiptap": "^3.0.1",
|
|
209
|
+
"sveltekit-superforms": "^2.28.1",
|
|
180
210
|
"ts-morph": "^26.0.0",
|
|
181
211
|
"tsx": "^4.20.3",
|
|
182
212
|
"typescript": "^5.0.0",
|
|
213
|
+
"tailwind-merge": "^3.3.1",
|
|
214
|
+
"tailwindcss": "^4.1.13",
|
|
183
215
|
"typescript-eslint": "^8.20.0",
|
|
184
216
|
"vite": "^7.2.2",
|
|
185
217
|
"vitest": "^3.2.3"
|
|
@@ -215,8 +247,6 @@
|
|
|
215
247
|
"@tiptap/starter-kit": "^3.17.1",
|
|
216
248
|
"@tiptap/suggestion": "^3.17.1",
|
|
217
249
|
"arctic": "^3.7.0",
|
|
218
|
-
"better-auth": "^1.3.34",
|
|
219
|
-
"bits-ui": "^2.11.5",
|
|
220
250
|
"dotenv": "^16.5.0",
|
|
221
251
|
"fast-glob": "^3.3.3",
|
|
222
252
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -224,24 +254,14 @@
|
|
|
224
254
|
"lowlight": "^3.3.0",
|
|
225
255
|
"nodemailer": "^7.0.11",
|
|
226
256
|
"openai": "^6.9.0",
|
|
227
|
-
"paneforge": "^1.0.2",
|
|
228
257
|
"path-to-regexp": "^8.2.0",
|
|
229
|
-
"postgres": "^3.4.5",
|
|
230
258
|
"readline": "^1.3.0",
|
|
231
|
-
"runed": "^0.35.1",
|
|
232
|
-
"sharp": "^0.34.2",
|
|
233
259
|
"slugify": "^1.6.6",
|
|
234
|
-
"svelte-sonner": "^1.0.5",
|
|
235
|
-
"svelte-tiptap": "^3.0.1",
|
|
236
260
|
"sveltekit-flash-message": "^2.4.6",
|
|
237
|
-
"sveltekit-superforms": "^2.28.1",
|
|
238
|
-
"tailwind-merge": "^3.3.1",
|
|
239
261
|
"tailwind-variants": "^3.1.1",
|
|
240
|
-
"tailwindcss": "^4.1.13",
|
|
241
262
|
"tippy.js": "^6.3.7",
|
|
242
263
|
"tw-animate-css": "^1.3.4",
|
|
243
|
-
"uuid": "^11.1.0"
|
|
244
|
-
"zod": "^4.0.0"
|
|
264
|
+
"uuid": "^11.1.0"
|
|
245
265
|
},
|
|
246
266
|
"pnpm": {
|
|
247
267
|
"onlyBuiltDependencies": [
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
-
return `Hello, ${i.name} from en!`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Welcome back`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
-
return `Login to your account`
|
|
14
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
-
return `Hello, ${i.name} from pl!`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Witaj ponownie`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
-
return `Zaloguj się na swoje konto`
|
|
14
|
-
};
|