@opengis/fastify-table 1.4.7 → 1.4.9

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.
Files changed (69) hide show
  1. package/config.js +1 -0
  2. package/index.js +5 -0
  3. package/package.json +8 -2
  4. package/server/plugins/file/downloadFile.js +18 -0
  5. package/server/plugins/file/getExport.js +38 -0
  6. package/server/plugins/file/isFileExists.js +17 -0
  7. package/server/plugins/file/providers/fs.js +100 -0
  8. package/server/plugins/file/providers/index.d.ts +49 -0
  9. package/server/plugins/file/providers/index.js +37 -0
  10. package/server/plugins/file/providers/mime/index.js +12 -0
  11. package/server/plugins/file/providers/mime/mimes.js +1180 -0
  12. package/server/plugins/file/providers/s3/client.js +41 -0
  13. package/server/plugins/file/providers/s3/funcs/downloadFile.js +50 -0
  14. package/server/plugins/file/providers/s3/funcs/fileExists.js +32 -0
  15. package/server/plugins/file/providers/s3/funcs/uploadFile.js +38 -0
  16. package/server/plugins/file/providers/s3/funcs/utils/getS3FilePath.js +23 -0
  17. package/server/plugins/file/providers/s3/index.js +12 -0
  18. package/server/plugins/file/providers/utils/getDataSize.js +20 -0
  19. package/server/plugins/file/providers/utils/getValidData.js +32 -0
  20. package/server/plugins/file/providers/utils/handlers/dataTypes.js +8 -0
  21. package/server/plugins/file/providers/utils/handlers/index.js +53 -0
  22. package/server/plugins/file/providers/utils/handlers/sizeHandlers.js +11 -0
  23. package/server/plugins/file/providers/utils/streamToBuffer.js +8 -0
  24. package/server/plugins/file/providers/utils/typeguards/isArray.js +3 -0
  25. package/server/plugins/file/providers/utils/typeguards/isBuffer.js +3 -0
  26. package/server/plugins/file/providers/utils/typeguards/isPath.js +5 -0
  27. package/server/plugins/file/providers/utils/typeguards/isReadableStream.js +8 -0
  28. package/server/plugins/file/providers/utils/typeguards/isText.js +3 -0
  29. package/server/plugins/file/uploadFile.js +19 -0
  30. package/server/plugins/file/uploadMultiPart.js +63 -0
  31. package/server/plugins/file/utils/allowedExtensions.js +25 -0
  32. package/server/plugins/file/utils/getFileType.js +10 -0
  33. package/server/plugins/file/utils/getPath.js +25 -0
  34. package/server/plugins/file/utils/isFileExists.js +15 -0
  35. package/server/plugins/file/utils/uploadFileDisk.js +87 -0
  36. package/server/plugins/grpc/file2json.js +54 -0
  37. package/server/plugins/grpc/grpc.js +295 -0
  38. package/server/plugins/grpc/office2pdf.js +68 -0
  39. package/server/plugins/grpc/utils/convertp.proto +137 -0
  40. package/server/plugins/grpc/utils/csv2xls.js +8 -0
  41. package/server/plugins/grpc/utils/excel2Json.js +8 -0
  42. package/server/plugins/grpc/utils/html2doc.js +19 -0
  43. package/server/plugins/grpc/utils/html2img.js +18 -0
  44. package/server/plugins/grpc/utils/html2pdf.js +23 -0
  45. package/server/plugins/grpc/utils/htmlTemplate.js +14 -0
  46. package/server/plugins/grpc/utils/json2xls.js +13 -0
  47. package/server/plugins/grpc/utils/mergePdf.js +20 -0
  48. package/server/plugins/grpc/utils/office2pdf.proto +14 -0
  49. package/server/plugins/table/funcs/getData.js +9 -3
  50. package/server/routes/file/controllers/delete.js +108 -0
  51. package/server/routes/file/controllers/download.js +66 -0
  52. package/server/routes/file/controllers/export.js +283 -0
  53. package/server/routes/file/controllers/files.js +72 -0
  54. package/server/routes/file/controllers/resize.js +90 -0
  55. package/server/routes/file/controllers/resizeAll.js +164 -0
  56. package/server/routes/file/controllers/upload.js +53 -0
  57. package/server/routes/file/controllers/uploadImage.js +47 -0
  58. package/server/routes/file/controllers/utils/convertJSONToCSV.js +36 -0
  59. package/server/routes/file/controllers/utils/convertJSONToXls.js +44 -0
  60. package/server/routes/file/controllers/utils/formatResult.js +17 -0
  61. package/server/routes/file/index.mjs +26 -0
  62. package/server/routes/file/schema.js +16 -0
  63. package/server/routes/grpc/controllers/file2geojson.js +60 -0
  64. package/server/routes/grpc/controllers/filePreview.js +89 -0
  65. package/server/routes/grpc/index.mjs +12 -0
  66. package/server/routes/table/controllers/cardData.js +10 -4
  67. package/server/routes/table/controllers/tableData.js +5 -5
  68. package/server/routes/table/functions/getData.js +21 -11
  69. package/utils.js +30 -2
@@ -1,8 +1,14 @@
1
1
  import routeData from '../../../routes/table/controllers/tableData.js';
2
2
 
3
- export default async function getData({ id, table, pg, filter, state, limit, page, search, user, order, sql, contextQuery, sufix }, reply, called) {
3
+ export default async function getData({
4
+ id, table, pg, headers, filter, state, limit, page, search, user, order, sql, contextQuery, sufix,
5
+ }, reply, called) {
4
6
  const params = { table, id };
5
- const query = { filter, limit, page, search, sql, state, order };
6
- const result = await routeData({ pg, params, query, user, contextQuery, sufix }, reply, called);
7
+ const query = {
8
+ filter, limit, page, search, sql, state, order,
9
+ };
10
+ const result = await routeData({
11
+ pg, headers, params, query, user, contextQuery, sufix,
12
+ }, reply, called);
7
13
  return result;
8
14
  }
@@ -0,0 +1,108 @@
1
+ import {
2
+ config, dataUpdate, logger, pgClients,
3
+ } from '../../../../utils.js';
4
+
5
+ const resp = { message: 'File not found', status: 404 };
6
+
7
+ /**
8
+ * Апі використовується для видалення файлів за допомогою fs або s3
9
+ *
10
+ * @method GET
11
+ * @summary Видалення файлів і оновлення логів в БД
12
+ * @priority 5
13
+ * @alias deleteFileAPI
14
+ * @type api
15
+ * @tag file
16
+ * @requires deleteFile
17
+ * @param {Object} params Параметри URL
18
+ * @param {String} params[0] Шлях до файла
19
+ * @errors 403,500
20
+ * @returns {Number} status Номер помилки
21
+ * @returns {String|Object} error Опис помилки
22
+ * @returns {Object} headers Заголовки HTTP
23
+ * @returns {String} message Повідомлення про успішне виконання або об'єкт з параметрами
24
+ */
25
+
26
+ export default async function deleteFileAPI(req) {
27
+ const {
28
+ pg = pgClients.client, params = {}, user = {},
29
+ } = req;
30
+
31
+ if (!params['*']) {
32
+ return resp;
33
+ }
34
+ const filename = params['*'].startsWith('/') || /^[0-9]+$/.test(params['*']) ? params['*'] : `/${params['*']}`;
35
+
36
+ const { uid, user_rnokpp: rnokpp } = user;
37
+
38
+ if (!filename) {
39
+ logger.file('file', {
40
+ level: 'INFO',
41
+ type: 'delete',
42
+ message: 'No required param "filename"',
43
+ file: params['*'],
44
+ uid,
45
+ rnokpp,
46
+ });
47
+ return resp;
48
+ }
49
+
50
+ if (filename.includes('..')) {
51
+ logger.file('file', {
52
+ level: 'WARN',
53
+ type: 'delete',
54
+ message: 'injection attempt',
55
+ file: params['*'],
56
+ uid,
57
+ rnokpp,
58
+ });
59
+ return resp;
60
+ }
61
+
62
+ try {
63
+ const result = await pg.query('select file_id, file_path as fpath, uid from crm.files where $1 in (file_path, file_id)', [filename])
64
+ .then((res1) => res1.rows?.[0] || {});
65
+
66
+ const res = await dataUpdate({
67
+ pg,
68
+ table: 'crm.files',
69
+ id: result?.file_id,
70
+ data: { file_status: 3 },
71
+ uid,
72
+ });
73
+
74
+ if (!res) {
75
+ logger.file('file', {
76
+ level: 'INFO',
77
+ type: 'delete',
78
+ message: resp.message,
79
+ file: params['*'],
80
+ uid,
81
+ rnokpp,
82
+ });
83
+ return resp;
84
+ }
85
+
86
+ const message = { id: res.file_id, filepath: res.file_path };
87
+ logger.file('file', {
88
+ level: 'INFO',
89
+ type: 'delete',
90
+ message,
91
+ file: params['*'],
92
+ uid,
93
+ rnokpp,
94
+ });
95
+ return { message, status: 200 };
96
+ }
97
+ catch (err) {
98
+ logger.file('file', {
99
+ level: 'ERROR',
100
+ type: 'delete',
101
+ message: err.toString(),
102
+ file: params['*'],
103
+ uid,
104
+ rnokpp,
105
+ });
106
+ return { error: config?.local ? err.toString() : 'Помилка видалення файлу', status: 500 };
107
+ }
108
+ }
@@ -0,0 +1,66 @@
1
+ import path from 'node:path';
2
+
3
+ import providers from '../../../plugins/file/providers/index.js';
4
+ import getMimeType from '../../../plugins/file/providers/mime/index.js';
5
+
6
+ /* const allowedPublicDirs = [
7
+ 'upload', 'page', 'site', 'maps', 'tmp', 'uploads', 'site_slider', 'module', 'product', 'image', 'geo_works_file',
8
+ ]; */
9
+
10
+ /**
11
+ * Апі використовується для скачування файлів за допомогою fs або s3
12
+ *
13
+ * @method GET
14
+ * @summary Апі для скачування файлів за провайдером
15
+ * @priority 4
16
+ * @alias download
17
+ * @type api
18
+ * @tag file
19
+ * @requires getMimeType
20
+ * @param {Object} params Параметри URL
21
+ * @errors 400,404,403,500
22
+ * @returns {Number} status Номер помилки
23
+ * @returns {String|Object} error Опис помилки
24
+ * @returns {Object} headers Заголовки HTTP
25
+ * @returns {String} pipe Шлях до файла для скачування або відображення
26
+ */
27
+
28
+ async function download({
29
+ params,
30
+ }, reply) {
31
+ if (!params?.['*']) return { message: 'not enough params', status: 400 };
32
+
33
+ const filename = params['*'].startsWith('/') ? params['*'].slice(1) : params['*'];
34
+ if (!filename) return { message: "required param 'filename'", status: 400 };
35
+
36
+ if (filename?.includes?.('..')) return { message: 'wrong params', status: 400 };
37
+
38
+ /* const { security } = getSettings();
39
+ const { enabled: externalAccess } = await pg.one('select enabled from admin.data_api where account_name = $1 and api_key = $2', { args: [account, key] });
40
+ if ((sid === 35 && !isUser && !externalAccess)
41
+ || (sid === 1
42
+ && !allowedPublicDirs.some((dir) => filename?.includes?.(`${dir}/`))
43
+ && !security?.public_dirs?.split(',')?.some((dir) => filename?.includes?.(`${dir}/`)))
44
+ ) {
45
+ return { error: 'Немає доступу', status: 403 };
46
+ } */
47
+
48
+ const filepath = filename.startsWith('files/')
49
+ ? filename
50
+ : path.join('files', filename);
51
+
52
+ // download
53
+ const fp = providers({});
54
+
55
+ const fileStream = await fp.downloadFile(filepath);
56
+ if (!fileStream) return { error: `Файл не знайдено - ${filename}`, status: 404 };
57
+
58
+ const headers = {
59
+ 'Content-Disposition': `attachment; filename=${path.basename(filename)}`,
60
+ 'Content-Type': getMimeType(filepath),
61
+ };
62
+ reply.headers(headers);
63
+ return fileStream;
64
+ }
65
+
66
+ export default download;
@@ -0,0 +1,283 @@
1
+ /* eslint-disable no-param-reassign */
2
+ /* eslint-disable no-plusplus */
3
+ /* eslint-disable no-await-in-loop */
4
+ /* eslint-disable no-nested-ternary */
5
+ import path from 'node:path';
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync } from 'node:fs';
8
+ import {
9
+ mkdir, readFile, rm, writeFile,
10
+ } from 'node:fs/promises';
11
+
12
+ import {
13
+ config, logger, getTemplate, getMeta, getFolder, pgClients, eventStream, getData, metaFormat,
14
+ } from '../../../../utils.js';
15
+
16
+ import convertJSONToXls from './utils/convertJSONToXls.js';
17
+ import convertJSONToCSV from './utils/convertJSONToCSV.js';
18
+ import formatResult from './utils/formatResult.js';
19
+
20
+ const startStreamWithTotal = 10000;
21
+ const rootDir = getFolder(config, 'local');
22
+
23
+ /**
24
+ * Експорт даних з таблиці
25
+ *
26
+ * @method GET
27
+ * @alias exportTable
28
+ * @type api
29
+ * @tag export
30
+ * @summary Експорт даних у таблицю(xlsx, csv, json, geojson)
31
+ * @priority 1
32
+ * @example
33
+ * /api/export?table=com_property.subjects.table&format=csv&cols=economy_type,name_ua
34
+ * @param {String} format Формат документу на виході
35
+ * @param {Boolean} nocache Чи використовувати кеш
36
+ * @param {String} table Таблиця в БД
37
+ * @param {String|Number} filter Параметр фільтру для застосування до експортованих даних
38
+ * @errors 400, 500
39
+ * @returns {Number} status Номер помилки
40
+ * @returns {String} error Опис помилки
41
+ * @returns {String|Object} message Повертає SQL запит або opt або рядки SQL запиту
42
+ * @returns {String} file Шлях до файла для скачування або відображення
43
+ */
44
+
45
+ export default async function exportTable({
46
+ pg = pgClients.client, user, unittest, columns: columns1, cls, query = {}, host = '127.0.0.1', tableSql, sourceName,
47
+ }, reply) {
48
+ const {
49
+ id, cols, search, format = 'json',
50
+ table, filter = 'empty', nocache,
51
+ formatAnswer = 'file', sql, stream,
52
+ } = query;
53
+
54
+ if (!table && !tableSql) {
55
+ return reply.status(400).send('not enough params: table');
56
+ }
57
+
58
+ if (!['csv', 'xlsx', 'json', 'geojson'].includes(format)) {
59
+ return reply.status(400).send('param format is invalid');
60
+ }
61
+
62
+ const date = new Date();
63
+ const sufixName = `${filter}-${cols || 'all'}-${search}-${query.limit || 'unlimited'}`;
64
+ const sufixDate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()].join('-');
65
+ const objInfo = createHash('md5').update([sufixName, sufixDate].join('-')).digest('hex');
66
+ const fileName = (table || (tableSql ? createHash('md5').update(tableSql).digest('hex') : '')).concat('_').concat(objInfo).concat('.').concat(format);
67
+
68
+ const filePath = path.join(rootDir, '/files/temp', fileName);
69
+ const ext = path.extname(filePath);
70
+ const cacheFile = existsSync(filePath);
71
+
72
+ const filePathJSON = ['csv', 'xlsx', 'geojson'].includes(format) ? filePath.replace(ext, '.json') : filePath;
73
+ const cacheFileJSON = existsSync(filePathJSON);
74
+
75
+ // return from cache
76
+ if (cacheFile && !sql && !nocache && !config.disableCache) {
77
+ return formatResult({
78
+ filePath, formatAnswer, folder: rootDir, reply,
79
+ });
80
+ }
81
+
82
+ // delete old file, prevent append
83
+ if (nocache || config.disableCache) {
84
+ if (cacheFile) await rm(filePath);
85
+ if (cacheFileJSON && format !== 'json') await rm(filePathJSON);
86
+ }
87
+
88
+ const loadTable = await getTemplate('table', table);
89
+
90
+ const meta = await getMeta({ pg, table: loadTable?.table || table });
91
+
92
+ if (!meta?.pk && !meta?.view && !tableSql) {
93
+ return reply.status(404).send('table not found');
94
+ }
95
+
96
+ if (format === 'geojson' && !meta?.geom) {
97
+ return reply.status(400).send('Ця форма не містить полів геометрії. Виберіть тип, який не потребує геометрії для вивантаження');
98
+ }
99
+
100
+ const options = {
101
+ id,
102
+ table,
103
+ pg,
104
+ filter,
105
+ search,
106
+ user,
107
+ sql,
108
+ sufix: false,
109
+ };
110
+
111
+ // check total count, debug sql etc.
112
+ const result = tableSql
113
+ ? await pg.query(`select count(*) as total, json_agg(row_to_json(q)) as rows from (${tableSql})q`).then(el => el.rows?.[0] || {})
114
+ : await getData(options, reply, true);
115
+
116
+ if (sql) return result;
117
+
118
+ if (!result?.rows?.length) {
119
+ return reply.status(200).send('Немає даних, які можна експортувати');
120
+ }
121
+
122
+ const { total, filtered = result.total || 0 } = result;
123
+
124
+ const limit = startStreamWithTotal > filtered
125
+ ? filtered
126
+ : (Math.min(query.limit || 1000, startStreamWithTotal) || startStreamWithTotal);
127
+
128
+ Object.assign(options, { limit });
129
+
130
+ const colmodel = (columns1 || loadTable?.columns || meta?.columns || [])?.map((el) => ({
131
+ name: el.name,
132
+ data: el.data || el.option,
133
+ title: el.title || el.ua,
134
+ type: el.type || el.format || 'text',
135
+ html: el.html,
136
+ })); // skip html to avoid errors
137
+
138
+ // get present columns
139
+ const columns = cols === 'all' || !cols ? colmodel : colmodel
140
+ ?.filter((el) => (el.type || /\./.test(el.name))
141
+ && !el?.hidden && (cols?.split(',')?.length ? cols.split(',').includes(el.name) : true))
142
+ ?.filter(el => (Object.hasOwn(el, 'export') ? !el.export : true));
143
+
144
+ const htmls = columns.filter(el => el.html).reduce((acc, curr) => ({ ...acc, [curr.name]: curr.html }), {});
145
+
146
+ const columnList = columns?.map((el) => (/\./.test(el.name)
147
+ ? `${el.name.split('.')[0]}->>'${el.name.split('.')[1]}' as ${el.name.split('.').pop()}` // check for json data
148
+ : el.name));
149
+
150
+ const send = (+filtered > startStreamWithTotal || stream) && !unittest
151
+ ? eventStream(reply)
152
+ : (unittest ? console.log : () => { });
153
+
154
+ // export xlsx / csv / json
155
+ const source = loadTable?.title || loadTable?.ua || table || sourceName;
156
+
157
+ const interval = setInterval(async () => {
158
+ send('process query...');
159
+ }, 5000);
160
+
161
+ // start stream only if total exceed limit, but use while anyway
162
+ const res = {};
163
+ let offset = 0;
164
+ let page = 1;
165
+ let seq = 0;
166
+
167
+ send(`Всього в реєстрі: ${result.total} (${filtered} з урахуванням фільтрів)`);
168
+
169
+ if (!cacheFileJSON || nocache || config.disableCache) {
170
+ while ((+filtered - offset > 0) && !res?.error) {
171
+ try {
172
+ send(`Оброблено: ${offset}/${filtered}`);
173
+
174
+ const { rows = [] } = tableSql
175
+ ? await pg.query(`select * from (${tableSql})q limit ${options.limit} offset ${offset}`)
176
+ : await getData({ ...options, page }, reply, true);
177
+ send(`seq: ${++seq}`);
178
+ send(`Обробка ${rows.length} об'єктів...`);
179
+
180
+ if (!rows.length) {
181
+ send('Обробка даних успішно завершена');
182
+ break;
183
+ }
184
+
185
+ await metaFormat({
186
+ rows, cls, htmls, sufix: false,
187
+ }, pg);
188
+ // skip non present after metaFormat
189
+ if (!tableSql) {
190
+ rows.forEach((row) => {
191
+ Object.keys(row).filter((el) => !columnList.includes(el)).forEach((key) => delete row[key]);
192
+ });
193
+ }
194
+
195
+ const jsonFileExists = existsSync(filePathJSON);
196
+
197
+ // convert from json to format
198
+ if (!jsonFileExists) { // if json not exists
199
+ await mkdir(path.dirname(filePath), { recursive: true });
200
+ await writeFile(filePathJSON, JSON.stringify(rows));
201
+ }
202
+ else { // if json exists
203
+ const jsonData = JSON.parse(await readFile(filePathJSON) || '{}');
204
+ const moreData = jsonData.concat(rows); // rewrite to appendFile?
205
+ await writeFile(filePathJSON, JSON.stringify(moreData));
206
+ }
207
+
208
+ offset += rows.length;
209
+ page++;
210
+ }
211
+ catch (err) {
212
+ send(`error: ${err.toString()}`);
213
+ logger.error('export/table', {
214
+ filePath: filePathJSON,
215
+ total,
216
+ filtered,
217
+ offset,
218
+ result: res,
219
+ error: err.toString(),
220
+ stack: err.stack,
221
+ });
222
+ Object.assign(res, { error: err.toString() });
223
+ }
224
+ }
225
+ }
226
+
227
+ clearInterval(interval);
228
+
229
+ if (res.error) {
230
+ send(res.error, 1);
231
+ return reply.status(500).send(res.error);
232
+ }
233
+
234
+ logger.file('export/table', {
235
+ table, format, total, filtered, time: Date.now() - date.getTime(),
236
+ });
237
+
238
+ if (format !== 'json') {
239
+ const txt = nocache || config.disableCache || !cacheFileJSON
240
+ ? `Сформовано файл формату json. Початок конвертації в ${format}...`
241
+ : `Знайдено файл формату json. Початок конвертації в ${format}...`;
242
+ send(txt);
243
+ }
244
+
245
+ if (format === 'geojson') {
246
+ const rows = JSON.parse(await readFile(filePathJSON) || '[]');
247
+
248
+ const geojson = {
249
+ type: 'FeatureCollection',
250
+ features: rows.map((row) => ({
251
+ type: 'Feature',
252
+ name: 'export',
253
+ geometry: row.geom,
254
+ properties: Object.fromEntries(Object.entries(row).filter(([key]) => key !== 'geom')),
255
+ })),
256
+ };
257
+
258
+ await mkdir(path.dirname(filePath), { recursive: true });
259
+ await writeFile(filePath, JSON.stringify(geojson));
260
+ }
261
+
262
+ const resp = {};
263
+
264
+ if (format === 'csv') {
265
+ await convertJSONToCSV({
266
+ filePath: filePathJSON, send, colmodel, domain: host, source, columnList,
267
+ });
268
+ }
269
+ if (format === 'xlsx') {
270
+ await convertJSONToXls({
271
+ filePath: filePathJSON, send, colmodel, domain: host, source, resp,
272
+ });
273
+ }
274
+
275
+ if (resp.error) {
276
+ return reply.status(resp.status || 500).send(resp.error);
277
+ }
278
+
279
+ send('Файл успішно сформовано. Натистіть кнопку ще раз для завантаження даних', 1);
280
+ return formatResult({
281
+ filePath, formatAnswer, folder: rootDir, reply,
282
+ });
283
+ }
@@ -0,0 +1,72 @@
1
+ import path from 'node:path';
2
+ import mime from '../../../plugins/file/providers/mime/index.js';
3
+
4
+ import isFileExists from '../../../plugins/file/isFileExists.js';
5
+ import downloadFile from '../../../plugins/file/downloadFile.js';
6
+
7
+ import { applyHook } from '../../../../utils.js';
8
+
9
+ /**
10
+ * Апі використовується для отримання різних файлів і можливістю змінювати їх
11
+ *
12
+ * @method GET
13
+ * @summary Отримання файлів з директорії системи
14
+ * @priority 5
15
+ * @alias files
16
+ * @type api
17
+ * @tag file
18
+ * @requires getFile
19
+ * @requires mime
20
+ * @param {Object} params Параметри URL
21
+ * @errors 500
22
+ * @returns {Number} status Номер помилки
23
+ * @returns {String} error Опис помилки
24
+ * @returns {Object} headers Заголовки HTTP
25
+ * @returns {String} pipe Шлях до файла для скачування або відображення
26
+ */
27
+
28
+ export default async function getFile({
29
+ params = {},
30
+ }, reply) {
31
+ if (!params?.['*']) {
32
+ return { message: 'not enough params', status: 400 };
33
+ }
34
+
35
+ if (params['*']?.includes('../')) {
36
+ return { message: 'wrong params', status: 403 };
37
+ }
38
+
39
+ const relpath = (params['*'].startsWith('/') ? params['*'].slice(1) : params['*']).replace(/files\//g, '') + (params['*'].endsWith('/') ? 'index.html' : '');
40
+
41
+ if (!relpath) {
42
+ return { message: 'No required param \'filename\'', status: 400 };
43
+ }
44
+
45
+ if (relpath.includes('..')) {
46
+ return { message: 'wrong params', status: 400 };
47
+ }
48
+
49
+ const hookData = await applyHook('preFile', { relpath, reply });
50
+ if (hookData) return hookData;
51
+
52
+ const filepath = path.join('files', relpath);
53
+
54
+ const exists = await isFileExists(filepath);
55
+ if (!exists) {
56
+ return { message: 'file not found', status: 404 };
57
+ }
58
+
59
+ const fileStream = await downloadFile(filepath);
60
+
61
+ if (!fileStream) {
62
+ return { message: 'file not found', status: 404 };
63
+ }
64
+
65
+ const headers = {
66
+ 'Content-Disposition': 'inline',
67
+ 'Cache-Control': 'max-age=86400',
68
+ 'Content-Type': `${mime(filepath)}`, // ; charset=utf-8 --- untested
69
+ };
70
+ reply.headers(headers);
71
+ return fileStream;
72
+ }
@@ -0,0 +1,90 @@
1
+ import path from 'node:path';
2
+ import { imageSize } from 'image-size';
3
+
4
+ import {
5
+ config, downloadFile, uploadFile, grpc, isFileExists,
6
+ } from '../../../../utils.js';
7
+
8
+ import getMimeType from '../../../plugins/file/providers/mime/index.js';
9
+
10
+ const defaultWidth = 400;
11
+ const defaultHeight = 240;
12
+
13
+ const getHeight = (width, size, ratio = 1) => {
14
+ if (size && size.toLowerCase().split('x')[1]) return size.toLowerCase().split('x')[1];
15
+ if (width) return width / ratio;
16
+ return defaultHeight;
17
+ };
18
+
19
+ const { resizeImage } = grpc();
20
+
21
+ /**
22
+ * Апі використовується для зміни розміру фото за шляхом
23
+ */
24
+
25
+ export default async function resize({ query = {}, unittest }, reply) {
26
+ const {
27
+ filepath, quality, size, w, h, nocache, maxWidth = 400,
28
+ } = query;
29
+
30
+ const basename = path.basename(filepath);
31
+ const mimeType = getMimeType(filepath);
32
+ const resizePath1 = size
33
+ ? filepath.replace(basename, `${size}_resized_${basename}`)
34
+ : filepath.replace(basename, `${w || defaultWidth}_${h || (w ? '' : defaultHeight)}_resized_${basename}`);
35
+
36
+ // get Resize Data
37
+ const fileExists = await isFileExists(resizePath1);
38
+ const originalFileExists = await isFileExists(resizePath1.replace('files/', 'files/original/')); // resize-all API compatibility
39
+
40
+ const resizePath = originalFileExists ? resizePath1.replace('files/', 'files/original/') : resizePath1;
41
+
42
+ const resizeData = fileExists ? await downloadFile(resizePath, { buffer: true }) : null;
43
+
44
+ if (resizeData && !config.disableCache && !nocache && !unittest) {
45
+ return reply.headers({ 'Content-Type': mimeType, 'Cache-control': 'max-age=604800' }).send(resizeData);
46
+ }
47
+
48
+ // get File Data
49
+ const fileData = await downloadFile(filepath, { buffer: true });
50
+
51
+ if (!fileData?.length) {
52
+ return reply.status(404).send(`Файл не знайдено - ${filepath}`);
53
+ }
54
+
55
+ const resizeQuality = Math.min(quality || 75, 100);
56
+
57
+ const { width = defaultWidth, height = defaultHeight } = imageSize(fileData) || {};
58
+
59
+ const ratio = width / height;
60
+
61
+ const check = (size?.toLowerCase?.()?.split?.('x') || []).concat(w, h).filter(el => el && +el > maxWidth);
62
+
63
+ if (check.length) {
64
+ return reply.status(400).send({ message: 'resize image size too big' });
65
+ }
66
+
67
+ const resizeWidth = (h && !w ? h * ratio : null)
68
+ || (size?.toLowerCase?.()?.split?.('x')?.[1] && !size?.toLowerCase?.()?.split?.('x')?.[0] ? size.toLowerCase().split('x')[1] * ratio : null)
69
+ || w
70
+ || size?.toLowerCase?.()?.split?.('x')?.[0]
71
+ || Math.min(width, maxWidth);
72
+
73
+ const resizeHeight = h || getHeight(resizeWidth, size, ratio);
74
+
75
+ if (config.trace) {
76
+ console.log('original width/height/ratio: ', width, height, ratio);
77
+ console.log('resize width/height/ratio/quality: ', resizeWidth, resizeHeight, resizeWidth / resizeHeight, resizeQuality);
78
+ }
79
+
80
+ const { result } = await resizeImage({
81
+ base64: Buffer.from(fileData).toString('base64'),
82
+ width: resizeWidth,
83
+ height: resizeHeight,
84
+ quality: resizeQuality,
85
+ });
86
+
87
+ await uploadFile(resizePath, Buffer.from(result, 'base64'));
88
+
89
+ return reply.headers({ 'Content-Type': mimeType }).send(Buffer.from(result, 'base64'));
90
+ }