feat: optionally gzip large save uploads with fallback (#5259) * feat: optionally gzip large save uploads with fallback * fix: replace Safari-prone save compression with fflate fallback * refactor: align save upload compression with review feedback * refactor: use compressRequest wrapper for save uploads * Refactor request compression settings * Fix default value * Avoid null in bytes parsing result * fix: switch request compression to fflate gzip * fix: add request compression maxBytes cap and clarify timeout semantics * Refresh package-lock.json * Unify payload limit setting names * Expose compression termination function * Add compression to group chat saves --------- Co-authored-by: Roland4396 <Roland4396@users.noreply.github.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -202,6 +202,16 @@ performance: | |||
| 202 | memoryCacheCapacity: '100mb' | 202 | memoryCacheCapacity: '100mb' |
| 203 | # Enables disk caching for character cards. Improves performances with large card libraries. | 203 | # Enables disk caching for character cards. Improves performances with large card libraries. |
| 204 | useDiskCache: true | 204 | useDiskCache: true |
| 205 | # Configures gzip compression for client requests with large payloads (e.g. settings or chat saves). | ||
| 206 | requestCompression: | ||
| 207 | # Enable request compression. | ||
| 208 | enabled: false | ||
| 209 | # Minimum payload size to trigger compression. Set to 0 to compress all requests regardless of size. | ||
| 210 | minPayloadSize: '256kb' | ||
| 211 | # Hard upper payload size limit for compression. Set to 0 to allow compression of any size. | ||
| 212 | maxPayloadSize: '8mb' | ||
| 213 | # Timeout for request compression in milliseconds. | ||
| 214 | timeout: 4000 | ||
| 205 | 215 | ||
| 206 | # CACHE BUSTER CONFIGURATION | 216 | # CACHE BUSTER CONFIGURATION |
| 207 | # IMPORTANT: Requires localhost or a domain with HTTPS, otherwise will not work! | 217 | # IMPORTANT: Requires localhost or a domain with HTTPS, otherwise will not work! |
| @@ -58,6 +58,7 @@ | |||
| 58 | "droll": "^0.2.1", | 58 | "droll": "^0.2.1", |
| 59 | "env-paths": "^3.0.0", | 59 | "env-paths": "^3.0.0", |
| 60 | "express": "^4.21.0", | 60 | "express": "^4.21.0", |
| 61 | "fflate": "^0.8.2", | ||
| 61 | "form-data": "^4.0.4", | 62 | "form-data": "^4.0.4", |
| 62 | "fuse.js": "^7.1.0", | 63 | "fuse.js": "^7.1.0", |
| 63 | "google-translate-api-x": "^10.7.2", | 64 | "google-translate-api-x": "^10.7.2", |
| @@ -5095,6 +5096,12 @@ | |||
| 5095 | "node": "^12.20 || >= 14.13" | 5096 | "node": "^12.20 || >= 14.13" |
| 5096 | } | 5097 | } |
| 5097 | }, | 5098 | }, |
| 5099 | "node_modules/fflate": { | ||
| 5100 | "version": "0.8.2", | ||
| 5101 | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", | ||
| 5102 | "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", | ||
| 5103 | "license": "MIT" | ||
| 5104 | }, | ||
| 5098 | "node_modules/file-entry-cache": { | 5105 | "node_modules/file-entry-cache": { |
| 5099 | "version": "6.0.1", | 5106 | "version": "6.0.1", |
| 5100 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", | 5107 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", |
| @@ -48,6 +48,7 @@ | |||
| 48 | "droll": "^0.2.1", | 48 | "droll": "^0.2.1", |
| 49 | "env-paths": "^3.0.0", | 49 | "env-paths": "^3.0.0", |
| 50 | "express": "^4.21.0", | 50 | "express": "^4.21.0", |
| 51 | "fflate": "^0.8.2", | ||
| 51 | "form-data": "^4.0.4", | 52 | "form-data": "^4.0.4", |
| 52 | "fuse.js": "^7.1.0", | 53 | "fuse.js": "^7.1.0", |
| 53 | "google-translate-api-x": "^10.7.2", | 54 | "google-translate-api-x": "^10.7.2", |
| @@ -23,6 +23,7 @@ import { toggle as slideToggle } from 'slidetoggle'; | |||
| 23 | import chalk from 'chalk'; | 23 | import chalk from 'chalk'; |
| 24 | import yaml from 'yaml'; | 24 | import yaml from 'yaml'; |
| 25 | import * as chevrotain from 'chevrotain'; | 25 | import * as chevrotain from 'chevrotain'; |
| 26 | import { gzipSync, gzip } from 'fflate'; | ||
| 26 | 27 | ||
| 27 | /** | 28 | /** |
| 28 | * Expose the libraries to the 'window' object. | 29 | * Expose the libraries to the 'window' object. |
| @@ -102,6 +103,8 @@ export default { | |||
| 102 | chalk, | 103 | chalk, |
| 103 | yaml, | 104 | yaml, |
| 104 | chevrotain, | 105 | chevrotain, |
| 106 | gzipSync, | ||
| 107 | gzip, | ||
| 105 | }; | 108 | }; |
| 106 | 109 | ||
| 107 | export { | 110 | export { |
| @@ -127,4 +130,6 @@ export { | |||
| 127 | chalk, | 130 | chalk, |
| 128 | yaml, | 131 | yaml, |
| 129 | chevrotain, | 132 | chevrotain, |
| 133 | gzipSync, | ||
| 134 | gzip, | ||
| 130 | }; | 135 | }; |
| @@ -285,6 +285,7 @@ import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js'; | |||
| 285 | import { MacroEngine } from './scripts/macros/engine/MacroEngine.js'; | 285 | import { MacroEngine } from './scripts/macros/engine/MacroEngine.js'; |
| 286 | import { addChatBackupsBrowser } from './scripts/chat-backups.js'; | 286 | import { addChatBackupsBrowser } from './scripts/chat-backups.js'; |
| 287 | import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js'; | 287 | import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js'; |
| 288 | import { compressRequest, setRequestCompressionConfig } from './scripts/request-compression.js'; | ||
| 288 | 289 | ||
| 289 | // API OBJECT FOR EXTERNAL WIRING | 290 | // API OBJECT FOR EXTERNAL WIRING |
| 290 | globalThis.SillyTavern = { | 291 | globalThis.SillyTavern = { |
| @@ -7131,7 +7132,7 @@ async function renamePastChats(oldAvatar, newAvatar, newName) { | |||
| 7131 | 7132 | ||
| 7132 | await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, currentChat, oldAvatar, newAvatar); | 7133 | await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, currentChat, oldAvatar, newAvatar); |
| 7133 | 7134 | ||
| 7134 | const saveChatResponse = await fetch('/api/chats/save', { | 7135 | const saveChatRequest = await compressRequest({ |
| 7135 | method: 'POST', | 7136 | method: 'POST', |
| 7136 | headers: getRequestHeaders(), | 7137 | headers: getRequestHeaders(), |
| 7137 | body: JSON.stringify({ | 7138 | body: JSON.stringify({ |
| @@ -7142,6 +7143,7 @@ async function renamePastChats(oldAvatar, newAvatar, newName) { | |||
| 7142 | }), | 7143 | }), |
| 7143 | cache: 'no-cache', | 7144 | cache: 'no-cache', |
| 7144 | }); | 7145 | }); |
| 7146 | const saveChatResponse = await fetch('/api/chats/save', saveChatRequest); | ||
| 7145 | 7147 | ||
| 7146 | if (!saveChatResponse.ok) { | 7148 | if (!saveChatResponse.ok) { |
| 7147 | throw new Error('Could not save chat'); | 7149 | throw new Error('Could not save chat'); |
| @@ -7225,7 +7227,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false } | |||
| 7225 | }; | 7227 | }; |
| 7226 | 7228 | ||
| 7227 | try { | 7229 | try { |
| 7228 | const result = await fetch('/api/chats/save', { | 7230 | const saveChatRequest = await compressRequest({ |
| 7229 | method: 'POST', | 7231 | method: 'POST', |
| 7230 | cache: 'no-cache', | 7232 | cache: 'no-cache', |
| 7231 | headers: getRequestHeaders(), | 7233 | headers: getRequestHeaders(), |
| @@ -7237,6 +7239,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false } | |||
| 7237 | force: force, | 7239 | force: force, |
| 7238 | }), | 7240 | }), |
| 7239 | }); | 7241 | }); |
| 7242 | const result = await fetch('/api/chats/save', saveChatRequest); | ||
| 7240 | 7243 | ||
| 7241 | if (result.ok) { | 7244 | if (result.ok) { |
| 7242 | return; | 7245 | return; |
| @@ -7729,6 +7732,7 @@ export async function getSettings() { | |||
| 7729 | 7732 | ||
| 7730 | accountStorage.init(settings?.accountStorage); | 7733 | accountStorage.init(settings?.accountStorage); |
| 7731 | await setUserControls(data.enable_accounts); | 7734 | await setUserControls(data.enable_accounts); |
| 7735 | setRequestCompressionConfig(data.request_compression); | ||
| 7732 | 7736 | ||
| 7733 | // Allow subscribers to mutate settings | 7737 | // Allow subscribers to mutate settings |
| 7734 | await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings); | 7738 | await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings); |
| @@ -7879,12 +7883,13 @@ export async function saveSettings(loopCounter = 0) { | |||
| 7879 | }; | 7883 | }; |
| 7880 | 7884 | ||
| 7881 | try { | 7885 | try { |
| 7882 | const result = await fetch('/api/settings/save', { | 7886 | const saveSettingsRequest = await compressRequest({ |
| 7883 | method: 'POST', | 7887 | method: 'POST', |
| 7884 | headers: getRequestHeaders(), | 7888 | headers: getRequestHeaders(), |
| 7885 | body: JSON.stringify(payload), | 7889 | body: JSON.stringify(payload), |
| 7886 | cache: 'no-cache', | 7890 | cache: 'no-cache', |
| 7887 | }); | 7891 | }); |
| 7892 | const result = await fetch('/api/settings/save', saveSettingsRequest); | ||
| 7888 | 7893 | ||
| 7889 | if (!result.ok) { | 7894 | if (!result.ok) { |
| 7890 | throw new Error(`Failed to save settings: ${result.statusText}`); | 7895 | throw new Error(`Failed to save settings: ${result.statusText}`); |
| @@ -34,6 +34,7 @@ import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsPro | |||
| 34 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 34 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 35 | import { createTagMapFromList } from './tags.js'; | 35 | import { createTagMapFromList } from './tags.js'; |
| 36 | import { renderTemplateAsync } from './templates.js'; | 36 | import { renderTemplateAsync } from './templates.js'; |
| 37 | import { compressRequest } from './request-compression.js'; | ||
| 37 | import { t } from './i18n.js'; | 38 | import { t } from './i18n.js'; |
| 38 | 39 | ||
| 39 | import { | 40 | import { |
| @@ -386,11 +387,12 @@ export async function convertSoloToGroupChat() { | |||
| 386 | } | 387 | } |
| 387 | 388 | ||
| 388 | // Save group chat | 389 | // Save group chat |
| 389 | const createChatResponse = await fetch('/api/chats/group/save', { | 390 | const createChatRequest = await compressRequest({ |
| 390 | method: 'POST', | 391 | method: 'POST', |
| 391 | headers: getRequestHeaders(), | 392 | headers: getRequestHeaders(), |
| 392 | body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }), | 393 | body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }), |
| 393 | }); | 394 | }); |
| 395 | const createChatResponse = await fetch('/api/chats/group/save', createChatRequest); | ||
| 394 | 396 | ||
| 395 | if (!createChatResponse.ok) { | 397 | if (!createChatResponse.ok) { |
| 396 | console.error('Group chat creation unsuccessful'); | 398 | console.error('Group chat creation unsuccessful'); |
| @@ -86,6 +86,7 @@ import { isExternalMediaAllowed } from './chats.js'; | |||
| 86 | import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 86 | import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 87 | import { t } from './i18n.js'; | 87 | import { t } from './i18n.js'; |
| 88 | import { accountStorage } from './util/AccountStorage.js'; | 88 | import { accountStorage } from './util/AccountStorage.js'; |
| 89 | import { compressRequest } from './request-compression.js'; | ||
| 89 | 90 | ||
| 90 | export { | 91 | export { |
| 91 | selected_group, | 92 | selected_group, |
| @@ -633,11 +634,12 @@ async function saveGroupChat(groupId, shouldSaveGroup, force = false) { | |||
| 633 | user_name: 'unused', | 634 | user_name: 'unused', |
| 634 | character_name: 'unused', | 635 | character_name: 'unused', |
| 635 | }; | 636 | }; |
| 636 | const response = await fetch('/api/chats/group/save', { | 637 | const saveGroupChatRequest = await compressRequest({ |
| 637 | method: 'POST', | 638 | method: 'POST', |
| 638 | headers: getRequestHeaders(), | 639 | headers: getRequestHeaders(), |
| 639 | body: JSON.stringify({ id: chatId, chat: [chatHeader, ...chat], force: force }), | 640 | body: JSON.stringify({ id: chatId, chat: [chatHeader, ...chat], force: force }), |
| 640 | }); | 641 | }); |
| 642 | const response = await fetch('/api/chats/group/save', saveGroupChatRequest); | ||
| 641 | 643 | ||
| 642 | if (!response.ok) { | 644 | if (!response.ok) { |
| 643 | const errorData = await response.json(); | 645 | const errorData = await response.json(); |
| @@ -728,11 +730,12 @@ export async function renameGroupMember(oldAvatar, newAvatar, newName) { | |||
| 728 | if (hadChanges) { | 730 | if (hadChanges) { |
| 729 | await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, messages, oldAvatar, newAvatar); | 731 | await eventSource.emit(event_types.CHARACTER_RENAMED_IN_PAST_CHAT, messages, oldAvatar, newAvatar); |
| 730 | 732 | ||
| 731 | const saveChatResponse = await fetch('/api/chats/group/save', { | 733 | const saveChatRequest = await compressRequest({ |
| 732 | method: 'POST', | 734 | method: 'POST', |
| 733 | headers: getRequestHeaders(), | 735 | headers: getRequestHeaders(), |
| 734 | body: JSON.stringify({ id: chatId, chat: [...messages] }), | 736 | body: JSON.stringify({ id: chatId, chat: [...messages] }), |
| 735 | }); | 737 | }); |
| 738 | const saveChatResponse = await fetch('/api/chats/group/save', saveChatRequest); | ||
| 736 | 739 | ||
| 737 | if (!saveChatResponse.ok) { | 740 | if (!saveChatResponse.ok) { |
| 738 | throw new Error('Group member could not be renamed'); | 741 | throw new Error('Group member could not be renamed'); |
| @@ -2374,11 +2377,12 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { | |||
| 2374 | 2377 | ||
| 2375 | await editGroup(groupId, true, false); | 2378 | await editGroup(groupId, true, false); |
| 2376 | 2379 | ||
| 2377 | const response = await fetch('/api/chats/group/save', { | 2380 | const saveChatRequest = await compressRequest({ |
| 2378 | method: 'POST', | 2381 | method: 'POST', |
| 2379 | headers: getRequestHeaders(), | 2382 | headers: getRequestHeaders(), |
| 2380 | body: JSON.stringify({ id: name, chat: [chatHeader, ...trimmedChat] }), | 2383 | body: JSON.stringify({ id: name, chat: [chatHeader, ...trimmedChat] }), |
| 2381 | }); | 2384 | }); |
| 2385 | const response = await fetch('/api/chats/group/save', saveChatRequest); | ||
| 2382 | 2386 | ||
| 2383 | if (!response.ok) { | 2387 | if (!response.ok) { |
| 2384 | toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be saved`); | 2388 | toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be saved`); |
| @@ -0,0 +1,131 @@ | |||
| 1 | import { gzip } from '/lib.js'; | ||
| 2 | |||
| 3 | /** | ||
| 4 | * @type {RequestCompressionConfig} | ||
| 5 | * | ||
| 6 | * @typedef {Object} RequestCompressionConfig | ||
| 7 | * @property {boolean} enabled Whether request compression is enabled. | ||
| 8 | * @property {number} minPayloadSize Minimum payload size in bytes to trigger compression. | ||
| 9 | * @property {number} maxPayloadSize Hard upper payload size limit for compression. | ||
| 10 | * @property {number} timeout Timeout for request compression in milliseconds. | ||
| 11 | */ | ||
| 12 | const requestCompressionConfig = { | ||
| 13 | enabled: false, | ||
| 14 | minPayloadSize: 0, | ||
| 15 | maxPayloadSize: 0, | ||
| 16 | timeout: 0, | ||
| 17 | }; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Sets the configuration for request compression from the server. | ||
| 21 | * @param {RequestCompressionConfig} config Configuration object for request compression | ||
| 22 | */ | ||
| 23 | export function setRequestCompressionConfig(config) { | ||
| 24 | Object.assign(requestCompressionConfig, (config ?? {})); | ||
| 25 | } | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Compresses a Uint8Array using gzip. | ||
| 29 | * @param {Uint8Array<ArrayBuffer>} input Uint8Array to compress | ||
| 30 | * @returns {{ promise: Promise<Uint8Array<ArrayBuffer>>, terminate: () => void }} Gzip-compressed Uint8Array promise and a terminate function. | ||
| 31 | */ | ||
| 32 | function gzipBuffer(input) { | ||
| 33 | let terminate = () => {}; | ||
| 34 | const promise = new Promise((resolve, reject) => { | ||
| 35 | try { | ||
| 36 | terminate = gzip(input, (error, compressed) => { | ||
| 37 | if (error) { | ||
| 38 | reject(error); | ||
| 39 | return; | ||
| 40 | } | ||
| 41 | |||
| 42 | resolve(new Uint8Array(compressed)); | ||
| 43 | }); | ||
| 44 | } catch (error) { | ||
| 45 | reject(error); | ||
| 46 | } | ||
| 47 | }); | ||
| 48 | return { promise, terminate }; | ||
| 49 | } | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Wraps a promise with a timeout, rejecting if the promise does not settle within the specified time. | ||
| 53 | * Note: timeout does not cancel the underlying compression task; it only stops waiting for it. | ||
| 54 | * @param {Promise<T>} promise Promise to wrap with a timeout | ||
| 55 | * @param {number} timeoutMs Timeout in milliseconds | ||
| 56 | * @param {string} label Used for error message if timeout occurs | ||
| 57 | * @returns {Promise<T>} Resolves with the original promise's value if it settles in time, otherwise rejects with a timeout error | ||
| 58 | * @template T Type of the promise's resolved value | ||
| 59 | */ | ||
| 60 | async function withTimeout(promise, timeoutMs, label) { | ||
| 61 | let timeoutId = null; | ||
| 62 | const timeoutPromise = new Promise((_, reject) => { | ||
| 63 | timeoutId = setTimeout(() => reject(new Error(`${label}_timeout`)), timeoutMs); | ||
| 64 | }); | ||
| 65 | |||
| 66 | try { | ||
| 67 | return await Promise.race([promise, timeoutPromise]); | ||
| 68 | } finally { | ||
| 69 | if (timeoutId !== null) { | ||
| 70 | clearTimeout(timeoutId); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Compresses a fetch request using gzip when supported and worthwhile. | ||
| 77 | * Compression is skipped when feature-toggle is disabled, body is too small, | ||
| 78 | * body is not a string, or compression fails/timeouts. | ||
| 79 | * | ||
| 80 | * @param {RequestInit} request fetch request parameters | ||
| 81 | * @returns {Promise<RequestInit>} A request init object that may include gzip-compressed body | ||
| 82 | */ | ||
| 83 | export async function compressRequest(request) { | ||
| 84 | const plainRequest = { ...request }; | ||
| 85 | const requestBody = plainRequest?.body; | ||
| 86 | |||
| 87 | if (!requestCompressionConfig.enabled) { | ||
| 88 | return plainRequest; | ||
| 89 | } | ||
| 90 | |||
| 91 | if (!requestBody || typeof requestBody !== 'string') { | ||
| 92 | return plainRequest; | ||
| 93 | } | ||
| 94 | |||
| 95 | const textEncoder = new TextEncoder(); | ||
| 96 | const encodedBody = textEncoder.encode(requestBody); | ||
| 97 | const bodySize = encodedBody.byteLength; | ||
| 98 | const minBytes = Number(requestCompressionConfig.minPayloadSize) || 0; | ||
| 99 | const maxBytes = Number(requestCompressionConfig.maxPayloadSize) || 0; | ||
| 100 | |||
| 101 | if (bodySize < minBytes || (maxBytes > 0 && bodySize > maxBytes)) { | ||
| 102 | return plainRequest; | ||
| 103 | } | ||
| 104 | |||
| 105 | const { promise, terminate } = gzipBuffer(encodedBody); | ||
| 106 | |||
| 107 | try { | ||
| 108 | const compressedBody = await withTimeout( | ||
| 109 | promise, | ||
| 110 | requestCompressionConfig.timeout, | ||
| 111 | 'compress_fflate_gzip', | ||
| 112 | ); | ||
| 113 | |||
| 114 | if (!compressedBody || compressedBody.byteLength >= bodySize) { | ||
| 115 | return plainRequest; | ||
| 116 | } | ||
| 117 | |||
| 118 | const headers = new Headers(plainRequest.headers ?? {}); | ||
| 119 | headers.set('Content-Encoding', 'gzip'); | ||
| 120 | |||
| 121 | return { | ||
| 122 | ...plainRequest, | ||
| 123 | headers, | ||
| 124 | body: compressedBody, | ||
| 125 | }; | ||
| 126 | } catch (error) { | ||
| 127 | terminate(); | ||
| 128 | console.warn('Failed to compress request body, using plain request.', error); | ||
| 129 | return plainRequest; | ||
| 130 | } | ||
| 131 | } | ||
| @@ -4,6 +4,7 @@ import path from 'node:path'; | |||
| 4 | import express from 'express'; | 4 | import express from 'express'; |
| 5 | import _ from 'lodash'; | 5 | import _ from 'lodash'; |
| 6 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 6 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 7 | import bytes from 'bytes'; | ||
| 7 | 8 | ||
| 8 | import { SETTINGS_FILE } from '../constants.js'; | 9 | import { SETTINGS_FILE } from '../constants.js'; |
| 9 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; | 10 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; |
| @@ -13,6 +14,10 @@ import { getFileNameValidationFunction } from '../middleware/validateFileName.js | |||
| 13 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true, 'boolean'); | 14 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true, 'boolean'); |
| 14 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true, 'boolean'); | 15 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true, 'boolean'); |
| 15 | const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean'); | 16 | const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean'); |
| 17 | const ENABLE_REQUEST_COMPRESSION = !!getConfigValue('performance.requestCompression.enabled', false, 'boolean'); | ||
| 18 | const REQUEST_COMPRESSION_MIN = bytes.parse(getConfigValue('performance.requestCompression.minPayloadSize', '256kb')); | ||
| 19 | const REQUEST_COMPRESSION_MAX = bytes.parse(getConfigValue('performance.requestCompression.maxPayloadSize', '8mb')); | ||
| 20 | const REQUEST_COMPRESSION_TIMEOUT = Number(getConfigValue('performance.requestCompression.timeout', 3000, 'number')); | ||
| 16 | 21 | ||
| 17 | // 10 minutes | 22 | // 10 minutes |
| 18 | const AUTOSAVE_INTERVAL = 10 * 60 * 1000; | 23 | const AUTOSAVE_INTERVAL = 10 * 60 * 1000; |
| @@ -281,6 +286,12 @@ router.post('/get', (request, response) => { | |||
| 281 | enable_extensions: ENABLE_EXTENSIONS, | 286 | enable_extensions: ENABLE_EXTENSIONS, |
| 282 | enable_extensions_auto_update: ENABLE_EXTENSIONS_AUTO_UPDATE, | 287 | enable_extensions_auto_update: ENABLE_EXTENSIONS_AUTO_UPDATE, |
| 283 | enable_accounts: ENABLE_ACCOUNTS, | 288 | enable_accounts: ENABLE_ACCOUNTS, |
| 289 | request_compression: { | ||
| 290 | enabled: ENABLE_REQUEST_COMPRESSION, | ||
| 291 | minPayloadSize: REQUEST_COMPRESSION_MIN || 0, | ||
| 292 | maxPayloadSize: REQUEST_COMPRESSION_MAX || 0, | ||
| 293 | timeout: REQUEST_COMPRESSION_TIMEOUT || 0, | ||
| 294 | }, | ||
| 284 | }); | 295 | }); |
| 285 | }); | 296 | }); |
| 286 | 297 | ||