@opengis/fastify-table 2.0.81 → 2.0.83
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/dist/server/plugins/file/utils/isFileExists.js +3 -3
- package/dist/server/plugins/metric/loggerSystem.d.ts.map +1 -1
- package/dist/server/plugins/metric/loggerSystem.js +34 -17
- package/dist/server/plugins/pg/pgClients.d.ts.map +1 -1
- package/dist/server/plugins/pg/pgClients.js +1 -0
- package/dist/server/routes/file/controllers/files.d.ts.map +1 -1
- package/dist/server/routes/file/controllers/resize.d.ts +1 -0
- package/dist/server/routes/file/controllers/resize.d.ts.map +1 -1
- package/dist/server/routes/file/controllers/resize.js +20 -6
- package/package.json +89 -88
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
2
|
import getPath from "./getPath.js";
|
|
3
3
|
const isFileExists = async (filepath) => {
|
|
4
4
|
const fullPath = getPath(filepath, { check: true });
|
|
5
5
|
if (!fullPath)
|
|
6
6
|
return false;
|
|
7
7
|
try {
|
|
8
|
-
await
|
|
9
|
-
return
|
|
8
|
+
const stats = await stat(fullPath);
|
|
9
|
+
return stats.isFile();
|
|
10
10
|
}
|
|
11
11
|
catch (err) {
|
|
12
12
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loggerSystem.d.ts","sourceRoot":"","sources":["../../../../server/plugins/metric/loggerSystem.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"loggerSystem.d.ts","sourceRoot":"","sources":["../../../../server/plugins/metric/loggerSystem.ts"],"names":[],"mappings":"AAgDA,wBAA8B,YAAY,CACxC,GAAG,CAAC,EAAE,GAAG,GACR,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAsJ9B"}
|
|
@@ -14,35 +14,50 @@ const filesFolder = getFolder(config);
|
|
|
14
14
|
const redisKey = "logger-process-online";
|
|
15
15
|
const sqlQuery = `select datname as dbname, application_name as app, client_addr as client_ip,
|
|
16
16
|
(now()-backend_start)::text as msec, state, query from pg_stat_activity where datname=$1`;
|
|
17
|
+
const prev = {};
|
|
18
|
+
const cpuUsage = () => {
|
|
19
|
+
const usage = process.cpuUsage();
|
|
20
|
+
const cpus = os.cpus();
|
|
21
|
+
const total = cpus.reduce((acc, curr) => acc + Object.values(curr.times).reduce((a, b) => a + b, 0), 0);
|
|
22
|
+
if (!prev.usage || !prev.total) {
|
|
23
|
+
Object.assign(prev, { usage, total });
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
const diffUsage = (usage.user - prev.usage.user + usage.system - prev.usage.system) / 1000;
|
|
27
|
+
const diffTotal = total - prev.total;
|
|
28
|
+
Object.assign(prev, { usage, total });
|
|
29
|
+
return (diffUsage / diffTotal) * cpus.length * 100;
|
|
30
|
+
};
|
|
31
|
+
cpuUsage();
|
|
17
32
|
export default async function loggerSystem(req) {
|
|
18
33
|
const { pg = pgClients.client } = req || {};
|
|
19
34
|
const dbName = pg?.options?.database;
|
|
20
35
|
const dbsize = config.redis
|
|
21
36
|
? await rclient.get(`${dbName}:content:dbsize:${redisKey}`)
|
|
22
37
|
: null;
|
|
23
|
-
const dbVerion =
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
const dbVerion = pg?.query
|
|
39
|
+
? await pg
|
|
40
|
+
.query("select version();")
|
|
41
|
+
.then((el) => el.rows?.[0]?.version)
|
|
42
|
+
: null;
|
|
26
43
|
const dbSize = dbsize ||
|
|
27
|
-
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
(pg?.query
|
|
45
|
+
? await pg
|
|
46
|
+
.query(`select pg_size_pretty(pg_database_size($1))`, [dbName])
|
|
47
|
+
.then((el) => el.rows?.[0]?.pg_size_pretty)
|
|
48
|
+
: null);
|
|
49
|
+
const query = pg?.query
|
|
50
|
+
? await pg.query(sqlQuery, [dbName]).then((el) => el.rows || [])
|
|
51
|
+
: null;
|
|
33
52
|
const { stdout: topProcess } = platform === "win32"
|
|
34
53
|
? { stdout: "Cant show top on this system type" }
|
|
35
54
|
: await execAsync("top -b -n 1");
|
|
36
55
|
const osInfo = `${os.version()} ${os.machine?.() || ""}`;
|
|
37
56
|
const cpuInfo = os.cpus();
|
|
38
|
-
const totalCpu = Object.values(cpuInfo?.[0]?.times || {}).reduce((acc, tv) => acc + tv, 0) *
|
|
39
|
-
cpuInfo.length;
|
|
40
57
|
const totalMemory = os.totalmem();
|
|
41
58
|
const memory = process.memoryUsage();
|
|
42
59
|
const resource = process.resourceUsage();
|
|
43
|
-
const currentCpuUsage =
|
|
44
|
-
totalCpu) *
|
|
45
|
-
100;
|
|
60
|
+
const currentCpuUsage = cpuUsage();
|
|
46
61
|
const redisInfo = config.redis ? await rclient.info() : "";
|
|
47
62
|
const lines = redisInfo.split("\r\n").filter((el) => el && el.split);
|
|
48
63
|
const redis = {};
|
|
@@ -56,9 +71,11 @@ export default async function loggerSystem(req) {
|
|
|
56
71
|
await rclient.set(`${dbName}:content:dbsize:${redisKey}`, dbSize, "EX", 30 * 60);
|
|
57
72
|
}
|
|
58
73
|
const latency = config.redis ? await rclient.latency("latest") : null;
|
|
59
|
-
const uptime =
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
const uptime = pg?.query
|
|
75
|
+
? await pg
|
|
76
|
+
.query("select extract('epoch' from current_timestamp - pg_postmaster_start_time())::int as uptime")
|
|
77
|
+
.then((el) => el.rows?.[0]?.uptime)
|
|
78
|
+
: null;
|
|
62
79
|
const metric5 = config.redis
|
|
63
80
|
? await rclient2.hgetall(`${dbName}:system_metrics`)
|
|
64
81
|
: {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pgClients.d.ts","sourceRoot":"","sources":["../../../../server/plugins/pg/pgClients.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"pgClients.d.ts","sourceRoot":"","sources":["../../../../server/plugins/pg/pgClients.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,CAAC;AAiB1C,eAAe,SAAS,CAAC"}
|
|
@@ -10,6 +10,7 @@ if (config.pg) {
|
|
|
10
10
|
user: config.pg?.user || "postgres",
|
|
11
11
|
password: config.pg?.password || "postgres",
|
|
12
12
|
statement_timeout: config.pg?.statement_timeout || 10000,
|
|
13
|
+
connectionTimeoutMillis: config.pg?.connectionTimeoutMillis || 5000,
|
|
13
14
|
});
|
|
14
15
|
client.init = async () => {
|
|
15
16
|
await init(client);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../../server/routes/file/controllers/files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAU5C;;;;;;;;;;;;;;;;;GAiBG;AAEH,wBAA8B,OAAO,CACnC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EACvC,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../../server/routes/file/controllers/files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAU5C;;;;;;;;;;;;;;;;;GAiBG;AAEH,wBAA8B,OAAO,CACnC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EACvC,KAAK,EAAE,YAAY,gBAgDpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../../../../server/routes/file/controllers/resize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../../../../server/routes/file/controllers/resize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA+B5C;;GAEG;AAEH,wBAA8B,MAAM,CAClC,EACE,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC1B,IAAI,EAAE,MAAM,CAAC;QACb,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,EACD,KAAK,EAAE,YAAY,kBAsHpB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import sharp from "sharp";
|
|
2
3
|
import { imageSize } from "image-size";
|
|
3
4
|
import { config, downloadFile, uploadFile, isFileExists, } from "../../../../utils.js";
|
|
4
5
|
import grpc from "../../../plugins/grpc/grpc.js";
|
|
@@ -18,7 +19,7 @@ const { resizeImage } = grpc();
|
|
|
18
19
|
* Апі використовується для зміни розміру фото за шляхом
|
|
19
20
|
*/
|
|
20
21
|
export default async function resize({ query, unittest, }, reply) {
|
|
21
|
-
const { filepath, quality, size, w, h, nocache } = query || {};
|
|
22
|
+
const { filepath, quality, size, w, h, nocache, format } = query || {};
|
|
22
23
|
if (!filepath) {
|
|
23
24
|
return reply.status(400).send("not enough query params: filepath");
|
|
24
25
|
}
|
|
@@ -31,17 +32,23 @@ export default async function resize({ query, unittest, }, reply) {
|
|
|
31
32
|
? filepath.replace(basename, `${size}_resized_${basename}`)
|
|
32
33
|
: filepath.replace(basename, `${w || defaultWidth}_${h || (w ? "" : defaultHeight)}_resized_${basename}`);
|
|
33
34
|
// get Resize Data
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const resizePath = originalFileExists
|
|
37
|
-
? resizePath1.replace("files/", "files/original/")
|
|
35
|
+
const resizePath2 = format === "webp"
|
|
36
|
+
? resizePath1.replace(path.extname(resizePath1), ".webp")
|
|
38
37
|
: resizePath1;
|
|
38
|
+
const fileExists = await isFileExists(resizePath2);
|
|
39
|
+
const originalFileExists = await isFileExists(resizePath2.replace("files/", "files/original/")); // resize-all API compatibility
|
|
40
|
+
const resizePath = originalFileExists
|
|
41
|
+
? resizePath2.replace("files/", "files/original/")
|
|
42
|
+
: resizePath2;
|
|
39
43
|
const resizeData = fileExists
|
|
40
44
|
? await downloadFile(resizePath, { buffer: true })
|
|
41
45
|
: null;
|
|
42
46
|
if (resizeData && !config.disableCache && !nocache && !unittest) {
|
|
43
47
|
return reply
|
|
44
|
-
.headers({
|
|
48
|
+
.headers({
|
|
49
|
+
"Content-Type": format === "webp" ? "image/webp" : mimeType,
|
|
50
|
+
"Cache-control": "max-age=604800",
|
|
51
|
+
})
|
|
45
52
|
.send(resizeData);
|
|
46
53
|
}
|
|
47
54
|
// get File Data
|
|
@@ -80,6 +87,13 @@ export default async function resize({ query, unittest, }, reply) {
|
|
|
80
87
|
quality: resizeQuality,
|
|
81
88
|
});
|
|
82
89
|
await uploadFile(resizePath, Buffer.from(result, "base64"));
|
|
90
|
+
if (format === "webp") {
|
|
91
|
+
const buffer = await sharp(Buffer.from(result, "base64"))
|
|
92
|
+
.webp({ quality: resizeQuality })
|
|
93
|
+
.toBuffer({ resolveWithObject: false });
|
|
94
|
+
await uploadFile(resizePath2, buffer);
|
|
95
|
+
return reply.headers({ "Content-Type": "image/webp" }).send(buffer);
|
|
96
|
+
}
|
|
83
97
|
return reply
|
|
84
98
|
.headers({ "Content-Type": mimeType })
|
|
85
99
|
.send(Buffer.from(result, "base64"));
|
package/package.json
CHANGED
|
@@ -1,88 +1,89 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@opengis/fastify-table",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "core-plugins",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"fastify",
|
|
8
|
-
"table",
|
|
9
|
-
"crud",
|
|
10
|
-
"auth",
|
|
11
|
-
"pg",
|
|
12
|
-
"backend"
|
|
13
|
-
],
|
|
14
|
-
"main": "dist/index.js",
|
|
15
|
-
"exports": {
|
|
16
|
-
".": "./dist/index.js",
|
|
17
|
-
"./index.js": "./dist/index.js",
|
|
18
|
-
"./utils.js": "./dist/utils.js"
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist/*"
|
|
22
|
-
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"prepublishOnly": "npm run build",
|
|
25
|
-
"clean": "tsc -b --clean",
|
|
26
|
-
"build": "tsc -b --clean && tsc && copyfiles server/plugins/grpc/utils/*.proto dist && copyfiles server/migrations/*.sql dist && copyfiles server/templates/**/*.html dist",
|
|
27
|
-
"prod": "NODE_ENV=production bun dist/server",
|
|
28
|
-
"patch": "npm version patch && git push && npm publish",
|
|
29
|
-
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
30
|
-
"test": "bun test",
|
|
31
|
-
"compress": "node compress.js",
|
|
32
|
-
"dev1": "bun --hot dist/server",
|
|
33
|
-
"dev": "NODE_ENV=production bun start",
|
|
34
|
-
"start": "LOG_LEVEL=trace bun server"
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"@aws-sdk/client-s3": "3.879.0",
|
|
38
|
-
"@aws-sdk/lib-storage": "3.879.0",
|
|
39
|
-
"@fastify/cookie": "11.0.2",
|
|
40
|
-
"@fastify/http-proxy": "11.1.2",
|
|
41
|
-
"@fastify/multipart": "9.0.3",
|
|
42
|
-
"@fastify/passport": "3.0.2",
|
|
43
|
-
"@fastify/rate-limit": "10.3.0",
|
|
44
|
-
"@fastify/session": "11.1.0",
|
|
45
|
-
"@grpc/grpc-js": "1.10.11",
|
|
46
|
-
"@grpc/proto-loader": "0.7.15",
|
|
47
|
-
"apache-crypt": "1.2.6",
|
|
48
|
-
"better-sqlite3": "12.2.0",
|
|
49
|
-
"dotenv": "16.5.0",
|
|
50
|
-
"fastify": "5.3.3",
|
|
51
|
-
"fastify-plugin": "5.0.1",
|
|
52
|
-
"fastify-session-redis-store": "7.1.2",
|
|
53
|
-
"handlebars": "4.7.8",
|
|
54
|
-
"image-size": "1.2.0",
|
|
55
|
-
"ioredis": "5.3.2",
|
|
56
|
-
"js-yaml": "4.1.0",
|
|
57
|
-
"markdown-it": "14.1.0",
|
|
58
|
-
"nodemailer": "7.0.6",
|
|
59
|
-
"otplib": "12.0.1",
|
|
60
|
-
"pg": "8.11.6",
|
|
61
|
-
"pino": "9.5.0",
|
|
62
|
-
"pino-abstract-transport": "2.0.0",
|
|
63
|
-
"promised-handlebars": "2.0.1",
|
|
64
|
-
"qrcode": "1.5.4",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"@types/
|
|
71
|
-
"@types/
|
|
72
|
-
"@types/
|
|
73
|
-
"@types/
|
|
74
|
-
"@types/
|
|
75
|
-
"@types/
|
|
76
|
-
"@types/passport
|
|
77
|
-
"@types/
|
|
78
|
-
"@types/
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"eslint
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengis/fastify-table",
|
|
3
|
+
"version": "2.0.83",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "core-plugins",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"fastify",
|
|
8
|
+
"table",
|
|
9
|
+
"crud",
|
|
10
|
+
"auth",
|
|
11
|
+
"pg",
|
|
12
|
+
"backend"
|
|
13
|
+
],
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.js",
|
|
17
|
+
"./index.js": "./dist/index.js",
|
|
18
|
+
"./utils.js": "./dist/utils.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/*"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"clean": "tsc -b --clean",
|
|
26
|
+
"build": "tsc -b --clean && tsc && copyfiles server/plugins/grpc/utils/*.proto dist && copyfiles server/migrations/*.sql dist && copyfiles server/templates/**/*.html dist",
|
|
27
|
+
"prod": "NODE_ENV=production bun dist/server",
|
|
28
|
+
"patch": "npm version patch && git push && npm publish",
|
|
29
|
+
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"compress": "node compress.js",
|
|
32
|
+
"dev1": "bun --hot dist/server",
|
|
33
|
+
"dev": "NODE_ENV=production bun start",
|
|
34
|
+
"start": "LOG_LEVEL=trace bun server"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@aws-sdk/client-s3": "3.879.0",
|
|
38
|
+
"@aws-sdk/lib-storage": "3.879.0",
|
|
39
|
+
"@fastify/cookie": "11.0.2",
|
|
40
|
+
"@fastify/http-proxy": "11.1.2",
|
|
41
|
+
"@fastify/multipart": "9.0.3",
|
|
42
|
+
"@fastify/passport": "3.0.2",
|
|
43
|
+
"@fastify/rate-limit": "10.3.0",
|
|
44
|
+
"@fastify/session": "11.1.0",
|
|
45
|
+
"@grpc/grpc-js": "1.10.11",
|
|
46
|
+
"@grpc/proto-loader": "0.7.15",
|
|
47
|
+
"apache-crypt": "1.2.6",
|
|
48
|
+
"better-sqlite3": "12.2.0",
|
|
49
|
+
"dotenv": "16.5.0",
|
|
50
|
+
"fastify": "5.3.3",
|
|
51
|
+
"fastify-plugin": "5.0.1",
|
|
52
|
+
"fastify-session-redis-store": "7.1.2",
|
|
53
|
+
"handlebars": "4.7.8",
|
|
54
|
+
"image-size": "1.2.0",
|
|
55
|
+
"ioredis": "5.3.2",
|
|
56
|
+
"js-yaml": "4.1.0",
|
|
57
|
+
"markdown-it": "14.1.0",
|
|
58
|
+
"nodemailer": "7.0.6",
|
|
59
|
+
"otplib": "12.0.1",
|
|
60
|
+
"pg": "8.11.6",
|
|
61
|
+
"pino": "9.5.0",
|
|
62
|
+
"pino-abstract-transport": "2.0.0",
|
|
63
|
+
"promised-handlebars": "2.0.1",
|
|
64
|
+
"qrcode": "1.5.4",
|
|
65
|
+
"sharp": "0.34.5",
|
|
66
|
+
"uglify-js": "3.19.3",
|
|
67
|
+
"undici": "7.16.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
71
|
+
"@types/bun": "^1.2.21",
|
|
72
|
+
"@types/js-yaml": "^4.0.9",
|
|
73
|
+
"@types/markdown-it": "^14.1.2",
|
|
74
|
+
"@types/node": "^24.3.1",
|
|
75
|
+
"@types/nodemailer": "^7.0.1",
|
|
76
|
+
"@types/passport": "^1.0.17",
|
|
77
|
+
"@types/passport-local": "^1.0.38",
|
|
78
|
+
"@types/pg": "^8.15.5",
|
|
79
|
+
"@types/qrcode": "^1.5.5",
|
|
80
|
+
"copyfiles": "^2.4.1",
|
|
81
|
+
"eslint": "^9.35.0",
|
|
82
|
+
"eslint-config-airbnb-extended": "^2.3.1",
|
|
83
|
+
"ts-migrate": "^0.1.35",
|
|
84
|
+
"typescript": "^5.9.2",
|
|
85
|
+
"vitest": "^3.2.4"
|
|
86
|
+
},
|
|
87
|
+
"author": "Softpro",
|
|
88
|
+
"license": "ISC"
|
|
89
|
+
}
|