decompress .json.zstd fetched by getSaveData
This commit is contained in:
@@ -10,11 +10,11 @@ export const defaultConfig = {
|
||||
// Size of a save if it is uploaded as uncompressed. The browser packs
|
||||
// saves as a stream, so these limits are about the backend's disk
|
||||
// rather than about memory.
|
||||
maxSizePlainTextBytes: 200 * 1024 * 1024,
|
||||
maxSizePlainTextMB: 200,
|
||||
// Archives produced by the game, recognized by the zip magic bytes.
|
||||
maxSizeCompressedBytes: 60 * 1024 * 1024,
|
||||
maxSizeCompressedMB: 60,
|
||||
// read COMPRESSION.md
|
||||
zstdCompressionLevel: 11,
|
||||
zstdCompressionLevel: 10,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const KILOBYTE = 1024
|
||||
const MEGABYTE = 1024 * KILOBYTE
|
||||
export const KILOBYTE = 1024
|
||||
export const MEGABYTE = 1024 * KILOBYTE
|
||||
|
||||
/**
|
||||
* Formats a byte count for error messages and status lines, e.g. "12.3 MB".
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import type { SaveUploadConfig } from '@/config/AppConfig'
|
||||
import { GAME_SAVE_ENTRY_NAMES, GameEnum } from '@/config/games'
|
||||
import { formatBytes } from '@/lib/formatBytes'
|
||||
import { formatBytes, MEGABYTE } from '@/lib/formatBytes'
|
||||
import { isZipFile } from './gameZip'
|
||||
import {
|
||||
OriginalFileType,
|
||||
@@ -68,9 +68,10 @@ export async function packSaveForUpload(
|
||||
onStage: StageCallback
|
||||
): Promise<PackedSave> {
|
||||
const isZip = await isZipFile(file)
|
||||
const sizeLimit = isZip
|
||||
? config.maxSizeCompressedBytes
|
||||
: config.maxSizePlainTextBytes
|
||||
// The limits are configured in megabytes, file sizes are in bytes.
|
||||
const sizeLimit =
|
||||
(isZip ? config.maxSizeCompressedMB : config.maxSizePlainTextMB) *
|
||||
MEGABYTE
|
||||
|
||||
if (file.size > sizeLimit) {
|
||||
const saveKind = isZip ? 'archived' : 'plain text'
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SaveUploadConfig } from '@/config/AppConfig'
|
||||
import { GameEnum } from '@/config/games'
|
||||
import { packSaveForUpload, StageCallback } from '@/lib/saveArchive/client'
|
||||
import { OriginalFileType } from '@/lib/saveArchive/messages'
|
||||
import { createZstdDecompressStream } from '@/lib/saveArchive/zstdStream'
|
||||
|
||||
export interface UploadSaveResponse {
|
||||
id: string
|
||||
@@ -86,7 +87,12 @@ export class SaveParserBackendService {
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
// Fetches the fully parsed data for a save once its parsing is done.
|
||||
/**
|
||||
* Fetches the fully parsed data for a save once its parsing is done.
|
||||
*
|
||||
* The backend stores and serves that data as zstd compressed json, so the
|
||||
* body is decompressed on the way in rather than read as text first.
|
||||
*/
|
||||
async getSaveData(id: string): Promise<Record<string, unknown>> {
|
||||
const url = `${this.API_BASE}/getSaveData?id=${encodeURIComponent(id)}`
|
||||
const response = await fetch(url)
|
||||
@@ -97,8 +103,16 @@ export class SaveParserBackendService {
|
||||
(await getBodyTextOrEmpty(response))
|
||||
)
|
||||
}
|
||||
// A body is only missing on a response that cannot carry one, which a
|
||||
// successful getSaveData never is.
|
||||
if (!response.body) {
|
||||
throw new Error('Failed to get parsed save: response has no body')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
// Decompresses as the bytes arrive, so the compressed json is never
|
||||
// held whole; only the parsed object stays in memory.
|
||||
const json = response.body.pipeThrough(createZstdDecompressStream())
|
||||
return await new Response(json).json()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user