diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index f6e363608b..7f1dd7e713 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -271,6 +271,7 @@ specifiers: mini-css-extract-plugin: ^2.2.0 minio: ^7.0.26 mongodb: ^4.1.1 + node-html-parser: ~5.3.3 pdfkit: ~0.13.0 postcss: ^8.3.4 postcss-load-config: ^3.1.0 @@ -279,6 +280,7 @@ specifiers: prettier-plugin-svelte: ^2.7.0 sass: ^1.53.0 sass-loader: ^12.1.0 + sharp: ~0.30.7 simplytyped: ^3.3.0 smartcrop: ~2.0.5 style-loader: ^3.2.1 @@ -571,6 +573,7 @@ dependencies: mini-css-extract-plugin: 2.6.1_webpack@5.73.0 minio: 7.0.28 mongodb: 4.7.0 + node-html-parser: 5.3.3 pdfkit: 0.13.0 postcss: 8.4.14 postcss-load-config: 3.1.4_postcss@8.4.14+ts-node@10.8.1 @@ -579,6 +582,7 @@ dependencies: prettier-plugin-svelte: 2.7.0_prettier@2.7.1+svelte@3.48.0 sass: 1.53.0 sass-loader: 12.6.0_sass@1.53.0+webpack@5.73.0 + sharp: 0.30.7 simplytyped: 3.3.0_typescript@4.7.4 smartcrop: 2.0.5 style-loader: 3.3.1_webpack@5.73.0 @@ -2182,6 +2186,12 @@ packages: '@types/node': 16.11.42 dev: false + /@types/sharp/0.30.4: + resolution: {integrity: sha512-6oJEzKt7wZeS7e+6x9QFEOWGs0T/6of00+0onZGN1zSmcSjcTDZKgIGZ6YWJnHowpaKUCFBPH52mYljWqU32Eg==} + dependencies: + '@types/node': 16.11.42 + dev: false + /@types/sockjs/0.3.33: resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} dependencies: @@ -3295,6 +3305,10 @@ packages: fsevents: 2.3.2 dev: false + /chownr/1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + /chrome-trace-event/1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} @@ -3381,6 +3395,21 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: false + /color-string/1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color/4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: false + /colorette/2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: false @@ -3765,6 +3794,11 @@ packages: which-typed-array: 1.1.8 dev: false + /deep-extend/0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: false @@ -3840,6 +3874,11 @@ packages: engines: {node: '>=8'} dev: false + /detect-libc/2.0.1: + resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} + engines: {node: '>=8'} + dev: false + /detect-newline/3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4561,6 +4600,11 @@ packages: engines: {node: '>= 0.8.0'} dev: false + /expand-template/2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + /expect/27.5.1: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -4943,6 +4987,10 @@ packages: get-intrinsic: 1.1.2 dev: false + /github-from-package/0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + /glob-escape/0.0.2: resolution: {integrity: sha512-L/cXYz8x7qer1HAyUQ+mbjcUsJVdpRxpAf7CwqHoNBs9vTpABlGfNN4tzkDxt+u3Z7ZncVyKlCNPtzb0R/7WbA==} engines: {node: '>= 0.10'} @@ -5379,6 +5427,10 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: false + /ini/1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + /internal-slot/1.0.3: resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} engines: {node: '>= 0.4'} @@ -5428,6 +5480,10 @@ packages: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false + /is-arrayish/0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + /is-bigint/1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: @@ -6740,6 +6796,10 @@ packages: xml2js: 0.4.23 dev: false + /mkdirp-classic/0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + /mkdirp/0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -6810,6 +6870,10 @@ packages: hasBin: true dev: false + /napi-build-utils/1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false @@ -6834,11 +6898,29 @@ packages: tslib: 2.4.0 dev: false + /node-abi/3.22.0: + resolution: {integrity: sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==} + engines: {node: '>=10'} + dependencies: + semver: 7.3.7 + dev: false + + /node-addon-api/5.0.0: + resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==} + dev: false + /node-forge/1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} dev: false + /node-html-parser/5.3.3: + resolution: {integrity: sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==} + dependencies: + css-select: 4.3.0 + he: 1.2.0 + dev: false + /node-int64/0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: false @@ -7344,6 +7426,25 @@ packages: source-map-js: 1.0.2 dev: false + /prebuild-install/7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.1 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.6 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.22.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /prelude-ls/1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -7612,6 +7713,16 @@ packages: unpipe: 1.0.0 dev: false + /rc/1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.6 + strip-json-comments: 2.0.1 + dev: false + /react-is/17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: false @@ -8056,6 +8167,21 @@ packages: kind-of: 6.0.3 dev: false + /sharp/0.30.7: + resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==} + engines: {node: '>=12.13.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.1 + node-addon-api: 5.0.0 + prebuild-install: 7.1.1 + semver: 7.3.7 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /shebang-command/2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -8080,6 +8206,24 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false + /simple-concat/1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get/4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + + /simple-swizzle/0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + /simplytyped/3.3.0_typescript@4.7.4: resolution: {integrity: sha512-mz4RaNdKTZiaKXgi6P1k/cdsxV3gz+y1Wh2NXHWD40dExktLh4Xx/h6MFakmQWODZHj/2rKe59acacpL74ZhQA==} peerDependencies: @@ -8344,6 +8488,11 @@ packages: min-indent: 1.0.1 dev: false + /strip-json-comments/2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -8542,6 +8691,15 @@ packages: engines: {node: '>=6'} dev: false + /tar-fs/2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + /tar-stream/2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -8784,6 +8942,12 @@ packages: typescript: 4.7.4 dev: false + /tunnel-agent/0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /type-check/0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} @@ -10284,7 +10448,7 @@ packages: dev: false file:projects/front.tgz: - resolution: {integrity: sha512-guStnV89AYnfgSdQuq+YfvZ2uemQ6ezSImVi7JN1WuwelfBmQAzUWS9FP4JndSlYagIhlk5eKrFOIqGFZQwcYA==, tarball: file:projects/front.tgz} + resolution: {integrity: sha512-2e41BziVvBMsiFDvITF0Qan/8+VHucvVUe6dXKlE4AXDktZlRm6lNdL+wvBFOvmni7/94wND5dva/Ukegy51cQ==, tarball: file:projects/front.tgz} name: '@rush-temp/front' version: 0.0.0 dependencies: @@ -10297,6 +10461,7 @@ packages: '@types/heft-jest': 1.0.3 '@types/minio': 7.0.13 '@types/node': 16.11.42 + '@types/sharp': 0.30.4 '@types/uuid': 8.3.4 '@typescript-eslint/eslint-plugin': 5.30.3_bd298502bfa44e376686f9e6b29811dd '@typescript-eslint/parser': 5.30.3_eslint@8.19.0+typescript@4.7.4 @@ -10314,6 +10479,7 @@ packages: express-fileupload: 1.4.0 minio: 7.0.28 prettier: 2.7.1 + sharp: 0.30.7 ts-node: 10.8.1_eff44b0165567fe5f2c2cc6c8fd30ef7 typescript: 4.7.4 uuid: 8.3.2 @@ -13914,7 +14080,7 @@ packages: dev: false file:projects/tool.tgz: - resolution: {integrity: sha512-7BKMRQ8UZ8cgJGDXvMGNoAdjUmEAyMx9GVBmBo3gmW56eLhxamaBqdwkunRIemCR77MJ8JJj+qhgWAzpRXpoRg==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-LjCQIq8dqLFEXvLcBmfJKRtkoJublg0r6DWC2RwIlmJyw61xEEBFHdOn3p1l3EyQPg0PjgAaSoEUPF+ViaxlPQ==, tarball: file:projects/tool.tgz} name: '@rush-temp/tool' version: 0.0.0 dependencies: @@ -13946,6 +14112,7 @@ packages: mime-types: 2.1.35 minio: 7.0.28 mongodb: 4.7.0 + node-html-parser: 5.3.3 prettier: 2.7.1 ts-node: 10.8.1_eff44b0165567fe5f2c2cc6c8fd30ef7 typescript: 4.7.4 diff --git a/packages/presentation/src/components/Avatar.svelte b/packages/presentation/src/components/Avatar.svelte index 8945d86e46..3d7a8814b8 100644 --- a/packages/presentation/src/components/Avatar.svelte +++ b/packages/presentation/src/components/Avatar.svelte @@ -29,7 +29,7 @@ url = blobURL }) } else if (avatar !== undefined && avatar !== null) { - url = getFileUrl(avatar) + url = getFileUrl(avatar, size) } else { url = undefined } diff --git a/packages/presentation/src/components/EditableAvatar.svelte b/packages/presentation/src/components/EditableAvatar.svelte index d4e039742a..43d5310416 100644 --- a/packages/presentation/src/components/EditableAvatar.svelte +++ b/packages/presentation/src/components/EditableAvatar.svelte @@ -35,7 +35,7 @@ if (direct !== undefined) { file = direct } else if (avatar != null) { - const url = getFileUrl(avatar) + const url = getFileUrl(avatar, 'full') file = await (await fetch(url)).blob() } else { return inputRef.click() diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index d53a8b415f..e9a873a864 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -38,6 +38,7 @@ import { getMetadata } from '@anticrm/platform' import { LiveQuery as LQ } from '@anticrm/query' import { onDestroy } from 'svelte' import { deepEqual } from 'fast-equals' +import { IconSize } from '@anticrm/ui' let liveQuery: LQ let client: TxOperations @@ -131,10 +132,10 @@ export function createQuery (dontDestroy?: boolean): LiveQuery { return new LiveQuery(dontDestroy) } -export function getFileUrl (file: string): string { +export function getFileUrl (file: string, size: IconSize = 'full'): string { const uploadUrl = getMetadata(login.metadata.UploadUrl) const token = getMetadata(login.metadata.LoginToken) - const url = `${uploadUrl as string}?file=${file}&token=${token as string}` + const url = `${uploadUrl as string}?file=${file}&token=${token as string}&size=${size as string}` return url } diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index 2848d04631..dc482ef29d 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -15,7 +15,7 @@ // import core, { Doc, Tx, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '@anticrm/core' -import type { TriggerControl } from '@anticrm/server-core' +import type { TriggerControl, MinioClient, BucketItem } from '@anticrm/server-core' import contact, { Contact, contactId, formatName, Organization, Person } from '@anticrm/contact' import { getMetadata } from '@anticrm/platform' import login from '@anticrm/login' @@ -52,6 +52,13 @@ export async function OnContactDelete (tx: Tx, { findAll, hierarchy, storageFx } storageFx(async (adapter, bucket) => { await adapter.removeObject(bucket, avatar) + + const extra = await listMinioObjects(adapter, bucket, avatar) + if (extra.size > 0) { + for (const e of extra.entries()) { + await adapter.removeObject(bucket, e[1].name) + } + } }) return [] @@ -105,3 +112,17 @@ export default async () => ({ OrganizationTextPresenter: organizationTextPresenter } }) + +async function listMinioObjects (client: MinioClient, db: string, prefix: string): Promise> { + const items = new Map() + const list = await client.listObjects(db, prefix, true) + await new Promise((resolve) => { + list.on('data', (data) => { + items.set(data.name, { ...data }) + }) + list.on('end', () => { + resolve(null) + }) + }) + return items +} diff --git a/server/core/src/types.ts b/server/core/src/types.ts index 234d983cd4..111f3b0d5e 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -34,7 +34,9 @@ import type { } from '@anticrm/core' import { Hierarchy, TxFactory } from '@anticrm/core' import type { Resource } from '@anticrm/platform' -import type { Client as MinioClient } from 'minio' +import type { Client as MinioClient, BucketItem } from 'minio' + +export { MinioClient, BucketItem } /** * @public diff --git a/server/front/Dockerfile b/server/front/Dockerfile index 8200f83b83..02df5dce77 100644 --- a/server/front/Dockerfile +++ b/server/front/Dockerfile @@ -1,9 +1,13 @@ -FROM node:16 +FROM node:16-alpine -WORKDIR /usr/src/app +RUN apk add dumb-init +ENV NODE_ENV production + +WORKDIR /app +RUN npm install --ignore-scripts=false --verbose sharp --unsafe-perm COPY bundle.js ./ COPY dist/ ./dist/ EXPOSE 8080 -CMD [ "node", "bundle.js" ] +CMD [ "dumb-init", "node", "./bundle.js" ] diff --git a/server/front/package.json b/server/front/package.json index 3acd2a9782..64efd2ec73 100644 --- a/server/front/package.json +++ b/server/front/package.json @@ -8,7 +8,7 @@ "build": "heft build", "build:watch": "tsc", "lint:fix": "eslint --fix src", - "bundle": "esbuild src/__start.ts --define:process.env.MODEL_VERSION=$(node ../../models/all/lib/__showversion.js) --bundle --minify --platform=node > bundle.js & rm -rf ./dist && cp -r ../../dev/prod/dist . && cp -r ../../dev/prod/public/* ./dist/ && rm ./dist/config.json", + "bundle": "esbuild src/__start.ts --define:process.env.MODEL_VERSION=$(node ../../models/all/lib/__showversion.js) --bundle --minify --platform=node --external:sharp > bundle.js & rm -rf ./dist && cp -r ../../dev/prod/dist . && cp -r ../../dev/prod/public/* ./dist/ && rm ./dist/config.json", "docker:build": "docker build -t hardcoreeng/front .", "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/front staging", "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/front", @@ -39,7 +39,8 @@ "@types/body-parser": "~1.19.2", "cross-env": "~7.0.3", "ts-node": "^10.8.0", - "@types/compression": "~1.7.2" + "@types/compression": "~1.7.2", + "@types/sharp": "~0.30.4" }, "dependencies": { "@anticrm/core": "~0.6.16", @@ -55,6 +56,7 @@ "@anticrm/contrib": "~0.6.0", "minio": "^7.0.26", "body-parser": "~1.19.1", - "compression": "~1.7.4" + "compression": "~1.7.4", + "sharp": "~0.30.7" } } diff --git a/server/front/src/app.ts b/server/front/src/app.ts index 4b7d6d3a19..e215d92d82 100644 --- a/server/front/src/app.ts +++ b/server/front/src/app.ts @@ -18,16 +18,17 @@ import attachment from '@anticrm/attachment' import { Account, Doc, Ref, Space } from '@anticrm/core' import { createElasticAdapter } from '@anticrm/elastic' import type { IndexedDoc } from '@anticrm/server-core' -import { decodeToken } from '@anticrm/server-token' +import { decodeToken, Token } from '@anticrm/server-token' import bp from 'body-parser' import compression from 'compression' import cors from 'cors' import express from 'express' import fileUpload, { UploadedFile } from 'express-fileupload' import https from 'https' -import { Client, ItemBucketMetadata } from 'minio' +import { BucketItem, Client, ItemBucketMetadata } from 'minio' import { join, resolve } from 'path' import { v4 as uuid } from 'uuid' +import sharp from 'sharp' async function minioUpload (minio: Client, workspace: string, file: UploadedFile): Promise { const id = uuid() @@ -41,6 +42,26 @@ async function minioUpload (minio: Client, workspace: string, file: UploadedFile return id } +async function readMinioData (client: Client, db: string, name: string): Promise { + const data = await client.getObject(db, name) + const chunks: Buffer[] = [] + + await new Promise((resolve) => { + data.on('readable', () => { + let chunk + while ((chunk = data.read()) !== null) { + const b = chunk as Buffer + chunks.push(b) + } + }) + + data.on('end', () => { + resolve(null) + }) + }) + return chunks +} + /** * @public * @param port - @@ -96,9 +117,13 @@ export function start ( try { const token = req.query.token as string const payload = decodeToken(token) - const uuid = req.query.file as string + let uuid = req.query.file as string + const size = req.query.size as 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' | 'full' + + uuid = await getResizeID(size, uuid, config, payload) const stat = await config.minio.statObject(payload.workspace, uuid) + config.minio.getObject(payload.workspace, uuid, function (err, dataStream) { if (err !== null) { return console.log(err) @@ -203,6 +228,13 @@ export function start ( await config.minio.removeObject(payload.workspace, uuid) + const extra = await listMinioObjects(config.minio, payload.workspace, uuid) + if (extra.size > 0) { + for (const e of extra.entries()) { + await config.minio.removeObject(payload.workspace, e[1].name) + } + } + res.status(200).send() } catch (error) { console.log(error) @@ -399,3 +431,66 @@ export function start ( server.close() } } +async function getResizeID (size: string, uuid: string, config: { minio: Client }, payload: Token): Promise { + if (size !== undefined && size !== 'full') { + let width = 64 + switch (size) { + case 'inline': + case 'tiny': + case 'x-small': + case 'small': + case 'medium': + width = 64 + break + case 'large': + width = 256 + break + case 'x-large': + width = 512 + break + } + let hasSmall = false + const sizeId = uuid + `%size%${width}` + try { + const d = await config.minio.statObject(payload.workspace, sizeId) + hasSmall = d !== undefined && d.size > 0 + } catch (err) {} + if (hasSmall) { + // We have cached small document, let's proceed with it. + uuid = sizeId + } else { + // Let's get data and resize it + const data = Buffer.concat(await readMinioData(config.minio, payload.workspace, uuid)) + + const dataBuff = await sharp(data) + .resize({ + width + }) + .jpeg() + .toBuffer() + await config.minio.putObject(payload.workspace, sizeId, dataBuff, { + 'Content-Type': 'image/jpeg' + }) + uuid = sizeId + } + } + return uuid +} + +async function listMinioObjects ( + client: Client, + db: string, + prefix: string +): Promise> { + const items = new Map() + const list = await client.listObjects(db, prefix, true) + await new Promise((resolve) => { + list.on('data', (data) => { + items.set(data.name, { metaData: {}, ...data }) + }) + list.on('end', () => { + resolve(null) + }) + }) + return items +}