@opengis/cms 0.0.34 → 0.0.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/cms",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "cms",
5
5
  "type": "module",
6
6
  "author": "Softpro",
package/plugin.js CHANGED
@@ -39,7 +39,7 @@ export default async function (app) {
39
39
  const pg = await getPGAsync(config.pg);
40
40
  execMigrations(path.join(dirname, 'server/migrations'), pg).catch(err => console.log(err));
41
41
  if (pg?.pk?.['site.content_types']) {
42
- const customTables = await pg.query('select array_agg(table_name) from site.content_types where table_name is not null').then(el => el.rows?.[0]?.array_agg || []).catch(err => console.log(err));
42
+ const customTables = await pg.query('select array_agg(table_name) from site.content_types where table_name is not null').catch(err => console.log(err)).then(el => el?.rows?.[0]?.array_agg || []);
43
43
 
44
44
  await Promise.all(customTables.filter(table => pg.pk?.[`data.${table}`])?.map(async (table) => {
45
45
  await pg.query(`alter table data.${table} add column if not exists meta json`).catch(err => console.log(err));
@@ -42,12 +42,12 @@ left join lateral (select content_type_id from site.content_types b where a.type
42
42
  -- seamless migration finish
43
43
 
44
44
  --- insert default content types start
45
- insert into site.content_types(content_type_id,name,title,type,columns)
45
+ insert into site.content_types(content_type_id,name,title,type,columns,template,uid)
46
46
  values('pages','pages','Сторінки','collection',
47
47
  '[{"name":"slug","label":"Slug","type":"slug","parent":"title","required":true},{"name":"title","label":"Title","type":"text","required":true},{"name":"status","label":"Status","type":"select","options":[{"id":"draft","text":"Draft"},{"id":"published","text":"Published"},{"id":"archived","text":"Archived"},{"id":"delayPublished","text":"Delay Published"}],"required":true},{"name":"published_at","label":"Publish at","type":"datetime","required":true}]'::json
48
- ) on conflict(content_type_id) do update set columns=excluded.columns, type = excluded.type;
49
- insert into site.contents(content_id,content_type_id,space_id)
50
- values('pages','pages','default') on conflict(content_id) do nothing;
48
+ ,'','1') on conflict(content_type_id) do update set columns=excluded.columns, type = excluded.type;
49
+ insert into site.contents(content_id,content_type_id,space_id,template,uid)
50
+ values('pages','pages','default','','1') on conflict(content_id) do nothing;
51
51
  --- insert default content types finish
52
52
 
53
53
  alter table site.contents add column if not exists meta json;
@@ -83,7 +83,8 @@ update site.content_types
83
83
  set columns=replace(columns::text,'publish_at', 'published_at')::json
84
84
  where columns::text like '%publish_at%';
85
85
 
86
- ALTER TABLE site.contents DROP CONSTRAINT contents_status_check;
86
+ ALTER TABLE site.contents DROP CONSTRAINT if exists contents_status_check;
87
+ -- select * from site.contents where not status = ANY (ARRAY['draft'::text, 'published'::text, 'archived'::text, 'delayPublished'::text])
87
88
  ALTER TABLE site.contents ADD CONSTRAINT contents_status_check CHECK (status = ANY (ARRAY['draft'::text, 'published'::text, 'archived'::text, 'delayPublished'::text]));
88
89
 
89
90
  UPDATE site.content_types
@@ -64,7 +64,7 @@ CREATE TABLE if not exists site.content_types (
64
64
  table_name VARCHAR(50),
65
65
  columns json,
66
66
  status VARCHAR (20) DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
67
- visible BOOLEAN DEFAULT TRUE,
67
+ visible BOOLEAN DEFAULT TRUE,
68
68
  localized BOOLEAN DEFAULT FALSE,
69
69
  type VARCHAR(20) NOT NULL DEFAULT 'collection' CHECK (type::text = ANY (ARRAY['collection', 'single']::text[])),
70
70
  schema JSONB,
@@ -78,6 +78,20 @@ CREATE TABLE if not exists site.content_types (
78
78
  updated_by text REFERENCES admin.users(uid)
79
79
  );
80
80
 
81
+ ALTER TABLE site.content_types add COLUMN if not exists table_name text;
82
+ ALTER TABLE site.content_types add COLUMN if not exists title text;
83
+ ALTER TABLE site.content_types add COLUMN if not exists status text;
84
+ ALTER TABLE site.content_types add COLUMN if not exists visible boolean default true;
85
+ ALTER TABLE site.content_types add COLUMN if not exists localized boolean default false;
86
+ ALTER TABLE site.content_types add COLUMN if not exists schema jsonb;
87
+ ALTER TABLE site.content_types add COLUMN if not exists icon text;
88
+ ALTER TABLE site.content_types add COLUMN if not exists color text;
89
+ ALTER TABLE site.content_types add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
90
+ ALTER TABLE site.content_types add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
91
+ ALTER TABLE site.content_types add COLUMN if not exists created_by text REFERENCES admin.users(uid);
92
+ ALTER TABLE site.content_types add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
93
+ ALTER TABLE site.content_types add COLUMN if not exists preview_path text;
94
+
81
95
  ALTER TABLE if exists site.content_types ALTER COLUMN table_name DROP NOT NULL;
82
96
  ALTER TABLE if exists site.content_types ADD COLUMN if not exists columns json;
83
97
 
@@ -145,6 +159,14 @@ CREATE TABLE if not exists site.menus (
145
159
  );
146
160
 
147
161
  alter table site.menus add column if not exists locale VARCHAR(20) not null default 'uk';
162
+ alter table site.menus add column if not exists content text;
163
+ alter table site.menus add column if not exists items json;
164
+ alter table site.menus add column if not exists description text;
165
+ ALTER TABLE site.menus add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
166
+ ALTER TABLE site.menus add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
167
+ ALTER TABLE site.menus add COLUMN if not exists created_by text REFERENCES admin.users(uid);
168
+ ALTER TABLE site.menus add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
169
+
148
170
  ALTER TABLE site.menus DROP CONSTRAINT if exists menus_name_key;
149
171
  ALTER TABLE site.menus DROP CONSTRAINT if exists menus_name_locale_unique;
150
172
  alter table site.menus drop column if exists lang;
@@ -189,8 +211,8 @@ CREATE INDEX if not exists idx_roles_name ON site.roles(name);
189
211
  -- drop table if exists site.permissions cascade;
190
212
  CREATE TABLE if not exists site.permissions (
191
213
  permission_id text PRIMARY KEY default next_id(),
192
- role_id text references site.roles(role_id),
193
- user_id text references admin.users(uid),
214
+ role_id text references site.roles(role_id),
215
+ user_id text references admin.users(uid),
194
216
  content_type_id text references site.content_types(content_type_id),
195
217
  subject VARCHAR(100) NOT NULL,
196
218
  actions TEXT[] NOT NULL check (actions && (ARRAY['read', 'create', 'delete', 'edit']::text[])),
@@ -229,7 +251,7 @@ CREATE TABLE if not exists site.tokens (
229
251
  last_used TIMESTAMP NULL, -- When the token was last used (nullable)
230
252
  created_at TIMESTAMP NOT NULL default now(), -- When the token was created
231
253
  updated_at TIMESTAMP NOT NULL default now(), -- When the token was updated
232
- created_by TEXT, -- Who created the token
254
+ created_by TEXT, -- Who created the token
233
255
  updated_by TEXT, -- When updated the token
234
256
  expiration_date TIMESTAMP, -- (nullable) Expiration date of the token
235
257
  token_type TEXT CHECK (token_type IN ('CMA', 'PAT')), -- Type of token
@@ -348,6 +370,19 @@ CREATE TABLE if not exists site.contents (
348
370
  FOREIGN KEY (updated_by) REFERENCES admin.users(uid)
349
371
  );
350
372
 
373
+ alter table site.contents add column if not exists space_id text;
374
+ alter table site.contents add column if not exists content_type_id text;
375
+ ALTER TABLE site.contents add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
376
+ ALTER TABLE site.contents add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
377
+ ALTER TABLE site.contents add COLUMN if not exists created_by text REFERENCES admin.users(uid);
378
+ ALTER TABLE site.contents add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
379
+ ALTER TABLE site.contents add COLUMN if not exists published_at TIMESTAMP DEFAULT NOW();
380
+ ALTER TABLE site.contents add COLUMN if not exists revision INTEGER NOT NULL DEFAULT 1;
381
+ ALTER TABLE site.contents add COLUMN if not exists locale TEXT NOT NULL DEFAULT 'uk'::text;
382
+ ALTER TABLE site.contents add COLUMN if not exists published_by text;
383
+ ALTER TABLE site.contents add COLUMN if not exists slug text;
384
+ ALTER TABLE site.contents add COLUMN if not exists meta json;
385
+
351
386
  COMMENT ON COLUMN site.contents.content_id IS 'Unique identifier for the content item (entry)';
352
387
  COMMENT ON COLUMN site.contents.space_id IS 'FK to the space this content belongs to';
353
388
  COMMENT ON COLUMN site.contents.content_type_id IS 'FK to the content type definition';
@@ -380,6 +415,21 @@ CREATE TABLE if not exists site.content_data (
380
415
  constraint content_data_content_id_fkey FOREIGN KEY (content_id) REFERENCES site.contents(content_id) on delete cascade
381
416
  );
382
417
 
418
+
419
+ alter table site.content_data add column if not exists field_key text not null;
420
+ alter table site.content_data add column if not exists field_type text;
421
+ alter table site.content_data add column if not exists field_value text;
422
+ alter table site.content_data add column if not exists field_value_object json;
423
+ alter table site.content_data add column if not exists locale text;
424
+ alter table site.content_data add column if not exists field_id text;
425
+ alter table site.content_data add column if not exists object_id text;
426
+ ALTER TABLE site.content_data add COLUMN if not exists created_at TIMESTAMP DEFAULT NOW();
427
+ ALTER TABLE site.content_data add COLUMN if not exists updated_at TIMESTAMP DEFAULT NOW();
428
+ ALTER TABLE site.content_data add COLUMN if not exists created_by text REFERENCES admin.users(uid);
429
+ ALTER TABLE site.content_data add COLUMN if not exists updated_by text REFERENCES admin.users(uid);
430
+
431
+
432
+
383
433
  -- create unique index for slug
384
434
  CREATE UNIQUE INDEX if not exists content_data_slug_unique ON site.content_data (field_value) WHERE field_key = 'slug';
385
435
 
@@ -1,77 +1,77 @@
1
- import path from 'node:path';
2
- import { existsSync } from 'node:fs';
3
- import { rm, stat, readdir } from 'node:fs/promises';
4
-
5
- import { config, dataDelete, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
6
-
7
- // path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
8
- const rootDir = path.resolve(getFolder(config, 'local'));
9
- const dir = '/files';
10
-
11
- export default async function deleteMedia({
12
- pg = pgClients.client, params = {}, query = {}, user = {}, method = 'DELETE',
13
- }, reply) {
14
- if (!config.debug && method !== 'DELETE') {
15
- return reply.status(403).send('access restricted');
16
- }
17
-
18
- if (!params.id && query.subdir) {
19
- const dirpath = path.join(rootDir, dir, query.subdir || '');
20
- const exists = existsSync(dirpath);
21
- const stats = await stat(dirpath);
22
-
23
- if (!stats.isDirectory()) {
24
- return reply.status(400).send('not a directory');
25
- }
26
-
27
- if (!exists) {
28
- return reply.status(404).send('subdir not found: ' + query.subdir);
29
- }
30
-
31
- const content = await readdir(dirpath, { recursive: true });
32
-
33
- if (query.subdir.startsWith('uploads') && !query.subdir.split('/')[1]) {
34
- return reply.status(403).send('access restricted: uploads directory');
35
- }
36
-
37
- // only admins are allowed to delete non-empty directories
38
- if (content?.length && !user?.user_type?.includes?.('admin')) {
39
- return reply.status(400).send('directory is not empty');
40
- }
41
-
42
- await rm(dirpath, { recursive: true });
43
- return reply.status(200).send('subdirectory successfully deleted');
44
- }
45
-
46
- if (!params?.id) {
47
- return reply.status(400).send('not enough params: id');
48
- }
49
-
50
- if (!pg.pk?.['site.media']) {
51
- return reply.status(404).send('table not found');
52
- }
53
-
54
- const { url: relpath, id } = await pg.query(
55
- 'select media_id as id, url from site.media where media_id = $1 and url is not null',
56
- [params.id],
57
- ).then(el => el.rows?.[0] || {});
58
-
59
- if (!id) {
60
- return reply.status(404).send('media not found: ' + params.id);
61
- }
62
-
63
- const res = await dataDelete({
64
- pg,
65
- id,
66
- table: 'site.media',
67
- uid: user?.uid || 0,
68
- });
69
-
70
- const filepath = path.join(rootDir, relpath);
71
-
72
- if (existsSync(filepath)) {
73
- await rm(filepath, { recursive: true });
74
- }
75
-
76
- return { id, ...res || {} };
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { rm, stat, readdir } from 'node:fs/promises';
4
+
5
+ import { config, dataDelete, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
6
+
7
+ // path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
8
+ const rootDir = path.resolve(getFolder(config, 'local'));
9
+ const dir = '/files';
10
+
11
+ export default async function deleteMedia({
12
+ pg = pgClients.client, params = {}, query = {}, user = {}, method = 'DELETE',
13
+ }, reply) {
14
+ if (!config.debug && method !== 'DELETE') {
15
+ return reply.status(403).send('access restricted');
16
+ }
17
+
18
+ if (!params.id && query.subdir) {
19
+ const dirpath = path.join(rootDir, dir, query.subdir || '');
20
+ const exists = existsSync(dirpath);
21
+ const stats = await stat(dirpath);
22
+
23
+ if (!stats.isDirectory()) {
24
+ return reply.status(400).send('not a directory');
25
+ }
26
+
27
+ if (!exists) {
28
+ return reply.status(404).send('subdir not found: ' + query.subdir);
29
+ }
30
+
31
+ const content = await readdir(dirpath, { recursive: true });
32
+
33
+ if (query.subdir.startsWith('uploads') && !query.subdir.split('/')[1]) {
34
+ return reply.status(403).send('access restricted: uploads directory');
35
+ }
36
+
37
+ // only admins are allowed to delete non-empty directories
38
+ if (content?.length && !user?.user_type?.includes?.('admin')) {
39
+ return reply.status(400).send('directory is not empty');
40
+ }
41
+
42
+ await rm(dirpath, { recursive: true });
43
+ return reply.status(200).send('subdirectory successfully deleted');
44
+ }
45
+
46
+ if (!params?.id) {
47
+ return reply.status(400).send('not enough params: id');
48
+ }
49
+
50
+ if (!pg.pk?.['site.media']) {
51
+ return reply.status(404).send('table not found');
52
+ }
53
+
54
+ const { url: relpath, id } = await pg.query(
55
+ 'select media_id as id, url from site.media where media_id = $1 and url is not null',
56
+ [params.id],
57
+ ).then(el => el.rows?.[0] || {});
58
+
59
+ if (!id) {
60
+ return reply.status(404).send('media not found: ' + params.id);
61
+ }
62
+
63
+ const res = await dataDelete({
64
+ pg,
65
+ id,
66
+ table: 'site.media',
67
+ uid: user?.uid || 0,
68
+ });
69
+
70
+ const filepath = path.join(rootDir, relpath);
71
+
72
+ if (existsSync(filepath)) {
73
+ await rm(filepath, { recursive: true });
74
+ }
75
+
76
+ return { id, ...res || {} };
77
77
  }
@@ -1,49 +1,49 @@
1
- import path from 'node:path';
2
- import { existsSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
-
5
- import { config, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
6
-
7
- // path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
8
- const rootDir = path.resolve(getFolder(config, 'local'));
9
-
10
- export default async function downloadMedia({
11
- pg = pgClients.client, params = {},
12
- }, reply) {
13
- if (!params?.id) {
14
- return reply.status(400).send('not enough params: id');
15
- }
16
-
17
- if (!pg.pk?.['site.media']) {
18
- return reply.status(404).send('table not found');
19
- }
20
-
21
- const { filename, mime, id, url: relpath } = await pg.query(
22
- 'select media_id as id, filename, mime, url from site.media where media_id = $1 and url is not null',
23
- [params.id],
24
- ).then(el => el.rows?.[0] || {});
25
-
26
- if (!id) {
27
- return reply.status(404).send('media not found: ' + params.id);
28
- }
29
-
30
- const filepath = path.join(rootDir, relpath);
31
-
32
- if (!existsSync(filepath)) {
33
- return reply.status(404).send('file not found');
34
- }
35
-
36
- const buffer = await readFile(filepath, { buffer: true });
37
-
38
- // skip xml load for preview
39
- if (params.type === 'preview' && path.extname(filename) !== '.xml') {
40
- return reply.headers({ 'Content-Type': mime }).send(buffer);
41
- }
42
-
43
- return reply
44
- .headers({
45
- 'Content-Type': mime,
46
- 'Content-Disposition': `attachment; filename=${filename || path.basename(filepath)}`,
47
- })
48
- .send(buffer);
49
- }
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ import { config, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
6
+
7
+ // path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
8
+ const rootDir = path.resolve(getFolder(config, 'local'));
9
+
10
+ export default async function downloadMedia({
11
+ pg = pgClients.client, params = {},
12
+ }, reply) {
13
+ if (!params?.id) {
14
+ return reply.status(400).send('not enough params: id');
15
+ }
16
+
17
+ if (!pg.pk?.['site.media']) {
18
+ return reply.status(404).send('table not found');
19
+ }
20
+
21
+ const { filename, mime, id, url: relpath } = await pg.query(
22
+ 'select media_id as id, filename, mime, url from site.media where media_id = $1 and url is not null',
23
+ [params.id],
24
+ ).then(el => el.rows?.[0] || {});
25
+
26
+ if (!id) {
27
+ return reply.status(404).send('media not found: ' + params.id);
28
+ }
29
+
30
+ const filepath = path.join(rootDir, relpath);
31
+
32
+ if (!existsSync(filepath)) {
33
+ return reply.status(404).send('file not found');
34
+ }
35
+
36
+ const buffer = await readFile(filepath, { buffer: true });
37
+
38
+ // skip xml load for preview
39
+ if (params.type === 'preview' && path.extname(filename) !== '.xml') {
40
+ return reply.headers({ 'Content-Type': mime }).send(buffer);
41
+ }
42
+
43
+ return reply
44
+ .headers({
45
+ 'Content-Type': mime,
46
+ 'Content-Disposition': `attachment; filename=${filename || path.basename(filepath)}`,
47
+ })
48
+ .send(buffer);
49
+ }
@@ -1,16 +1,16 @@
1
- import { pgClients } from '@opengis/fastify-table/utils.js';
2
-
3
- export default async function getPermissions(req, reply) {
4
- const { pg = pgClients.client, params = {}, user = {} } = req;
5
-
6
- if (!user?.uid) {
7
- return reply.status(401).send('unauthorized');
8
- }
9
-
10
- const { rows = [] } = await pg.query(
11
- `select * from site.permissions where ${params.id ? 'user_id=$1' : 'true'}`,
12
- [params.id].filter(Boolean),
13
- );
14
-
15
- return { permissions: rows };
1
+ import { pgClients } from '@opengis/fastify-table/utils.js';
2
+
3
+ export default async function getPermissions(req, reply) {
4
+ const { pg = pgClients.client, params = {}, user = {} } = req;
5
+
6
+ if (!user?.uid) {
7
+ return reply.status(401).send('unauthorized');
8
+ }
9
+
10
+ const { rows = [] } = await pg.query(
11
+ `select * from site.permissions where ${params.id ? 'user_id=$1' : 'true'}`,
12
+ [params.id].filter(Boolean),
13
+ );
14
+
15
+ return { permissions: rows };
16
16
  }
@@ -1,95 +1,95 @@
1
- import path from 'node:path';
2
- import { existsSync, mkdirSync } from 'node:fs';
3
- import { readdir, stat } from 'node:fs/promises';
4
-
5
- import { config, getFolder, pgClients, getMimeType } from '@opengis/fastify-table/utils.js';
6
- import { createHash } from 'node:crypto';
7
-
8
- // path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
9
- const rootDir = path.resolve(getFolder(config, 'local'));
10
- const dir = '/files';
11
-
12
- mkdirSync(path.join(rootDir, dir), { recursive: true });
13
-
14
- export default async function listMedia(req, reply) {
15
- const { pg = pgClients.client, query = {} } = req;
16
- const { subdir: subdir1 = '', search } = query;
17
-
18
- if (!pg.pk?.['site.media']) {
19
- return reply.status(404).send('table not found');
20
- }
21
-
22
- if (typeof subdir1 !== 'string' || subdir1.includes('..')) {
23
- return reply.status(403).send('invalid params: subdir');
24
- }
25
-
26
- const subdir = subdir1.replace(/\/{2,}/g, '/');
27
- const relpath = path.join(dir, subdir1).replace(/\\/g, '/');
28
- const dirpath = path.join(rootDir, relpath).replace(/\\/g, '/');
29
-
30
- if (!existsSync(dirpath)) {
31
- return { data: [], relpath, msg: 'directory not exists' };
32
- }
33
-
34
- const isDirectory = (await stat(dirpath)).isDirectory();
35
-
36
- const allItems = isDirectory ? await readdir(dirpath, { withFileTypes: true, recursive: true }) : [];
37
- const items = isDirectory ? await readdir(dirpath, { withFileTypes: true }) : [];
38
-
39
- const rows = await pg.query(
40
- `select
41
- media_id as id, filename, filetype, filesize, url, description, alt,
42
- mime, preview_url, created_at, updated_at, created_by, updated_by
43
- from site.media
44
- where ${subdir ? 'subdir = $1' : (search ? '1=1' : 'subdir is null')} ${search ? `and filename ilike '%${search.replace(/'/g, "''")}%'` : ''}`,
45
- [subdir].filter(Boolean),
46
- ).then(el => el.rows || []); // ?.filter(row => items.map(el => el.name).includes(row.filename))
47
-
48
- const subdirs = items
49
- .filter(el => el.isDirectory())
50
- .map(el => ({ type: 'dir', name: el.name }))
51
- .filter(el => search ? el.name.includes(search) : true);
52
-
53
- if (subdirs.length) {
54
- await Promise.all(subdirs.map(async (item) => {
55
- const items = isDirectory ? await readdir(path.join(dirpath, item.name)) : [];
56
- Object.assign(item, { count: items.length });
57
- }));
58
- }
59
-
60
- const result = { relpath };
61
-
62
- if (config.debug) {
63
- Object.assign(result, { rootDir });
64
- }
65
-
66
- const files = (search ? allItems.filter(el => el.name.includes(search)) : items)
67
- .filter(el => el.isFile())
68
- .map(el => {
69
- const media = rows.find(row => row.filename === el.name);
70
- const filepath = media ? media.url : ('/files/' + (el.path.split('/files/')[1] ? el.path.split('/files/')[1] + '/' : '') + el.name);
71
- return media ? {
72
- type: 'file',
73
- ...media,
74
- url: `${req.routeOptions.url}/${media.id}/file`,
75
- preview: `${req.routeOptions.url}/${media.id}/preview`,
76
- filepath,
77
- metadata: `${req.routeOptions.url}/${media.id}`,
78
- } : {
79
- id: createHash('md5').update(filepath).digest('hex'),
80
- hash: true,
81
- type: 'file',
82
- filename: el.name,
83
- filepath,
84
- filetype: getMimeType(el.name)?.startsWith('image/') ? "image" : "other",
85
- filesize: 0,
86
- url: filepath,
87
- mime: getMimeType(el.name),
88
- preview: filepath,
89
- }
90
- });
91
-
92
- Object.assign(result, { data: subdirs.concat(files) });
93
- return result;
94
-
1
+ import path from 'node:path';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { readdir, stat } from 'node:fs/promises';
4
+
5
+ import { config, getFolder, pgClients, getMimeType } from '@opengis/fastify-table/utils.js';
6
+ import { createHash } from 'node:crypto';
7
+
8
+ // path.resolve() converts POSIX paths from getFolder to valid Windows paths (Bun/Node fs require this on Windows)
9
+ const rootDir = path.resolve(getFolder(config, 'local'));
10
+ const dir = '/files';
11
+
12
+ mkdirSync(path.join(rootDir, dir), { recursive: true });
13
+
14
+ export default async function listMedia(req, reply) {
15
+ const { pg = pgClients.client, query = {} } = req;
16
+ const { subdir: subdir1 = '', search } = query;
17
+
18
+ if (!pg.pk?.['site.media']) {
19
+ return reply.status(404).send('table not found');
20
+ }
21
+
22
+ if (typeof subdir1 !== 'string' || subdir1.includes('..')) {
23
+ return reply.status(403).send('invalid params: subdir');
24
+ }
25
+
26
+ const subdir = subdir1.replace(/\/{2,}/g, '/');
27
+ const relpath = path.join(dir, subdir1).replace(/\\/g, '/');
28
+ const dirpath = path.join(rootDir, relpath).replace(/\\/g, '/');
29
+
30
+ if (!existsSync(dirpath)) {
31
+ return { data: [], relpath, msg: 'directory not exists' };
32
+ }
33
+
34
+ const isDirectory = (await stat(dirpath)).isDirectory();
35
+
36
+ const allItems = isDirectory ? await readdir(dirpath, { withFileTypes: true, recursive: true }) : [];
37
+ const items = isDirectory ? await readdir(dirpath, { withFileTypes: true }) : [];
38
+
39
+ const rows = await pg.query(
40
+ `select
41
+ media_id as id, filename, filetype, filesize, url, description, alt,
42
+ mime, preview_url, created_at, updated_at, created_by, updated_by
43
+ from site.media
44
+ where ${subdir ? 'subdir = $1' : (search ? '1=1' : 'subdir is null')} ${search ? `and filename ilike '%${search.replace(/'/g, "''")}%'` : ''}`,
45
+ [subdir].filter(Boolean),
46
+ ).then(el => el.rows || []); // ?.filter(row => items.map(el => el.name).includes(row.filename))
47
+
48
+ const subdirs = items
49
+ .filter(el => el.isDirectory())
50
+ .map(el => ({ type: 'dir', name: el.name }))
51
+ .filter(el => search ? el.name.includes(search) : true);
52
+
53
+ if (subdirs.length) {
54
+ await Promise.all(subdirs.map(async (item) => {
55
+ const items = isDirectory ? await readdir(path.join(dirpath, item.name)) : [];
56
+ Object.assign(item, { count: items.length });
57
+ }));
58
+ }
59
+
60
+ const result = { relpath };
61
+
62
+ if (config.debug) {
63
+ Object.assign(result, { rootDir });
64
+ }
65
+
66
+ const files = (search ? allItems.filter(el => el.name.includes(search)) : items)
67
+ .filter(el => el.isFile())
68
+ .map(el => {
69
+ const media = rows.find(row => row.filename === el.name);
70
+ const filepath = media ? media.url : ('/files/' + (el.path.split('/files/')[1] ? el.path.split('/files/')[1] + '/' : '') + el.name);
71
+ return media ? {
72
+ type: 'file',
73
+ ...media,
74
+ url: `${req.routeOptions.url}/${media.id}/file`,
75
+ preview: `${req.routeOptions.url}/${media.id}/preview`,
76
+ filepath,
77
+ metadata: `${req.routeOptions.url}/${media.id}`,
78
+ } : {
79
+ id: createHash('md5').update(filepath).digest('hex'),
80
+ hash: true,
81
+ type: 'file',
82
+ filename: el.name,
83
+ filepath,
84
+ filetype: getMimeType(el.name)?.startsWith('image/') ? "image" : "other",
85
+ filesize: 0,
86
+ url: filepath,
87
+ mime: getMimeType(el.name),
88
+ preview: filepath,
89
+ }
90
+ });
91
+
92
+ Object.assign(result, { data: subdirs.concat(files) });
93
+ return result;
94
+
95
95
  }