Skip to content

Commit 5268c51

Browse files
authored
Merge pull request #16690 from Budibase/revert-16653-tar-cpu
Revert "Convert app import/export to be fully streamed end to end."
2 parents e8d9a59 + c34ac92 commit 5268c51

File tree

16 files changed

+385
-581
lines changed

16 files changed

+385
-581
lines changed

packages/backend-core/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@
7272
"rotating-file-stream": "3.1.0",
7373
"sanitize-s3-objectkey": "0.0.1",
7474
"semver": "^7.5.4",
75-
"tar-fs": "2.1.3",
76-
"tar-stream": "3.1.7",
75+
"tar-fs": "2.1.2",
7776
"uuid": "^8.3.2"
7877
},
7978
"devDependencies": {

packages/backend-core/src/db/couch/DatabaseImpl.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ import {
2020
import { getCouchInfo } from "./connections"
2121
import { directCouchUrlCall } from "./utils"
2222
import { getPouchDB } from "./pouchDB"
23-
import { WriteStream } from "fs"
23+
import { ReadStream, WriteStream } from "fs"
2424
import { newid } from "../../docIds/newid"
2525
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
2626
import { DDInstrumentedDatabase } from "../instrumentation"
2727
import { checkSlashesInUrl } from "../../helpers"
2828
import { sqlLog } from "../../sql/utils"
29-
import { Readable } from "stream"
3029

3130
const DATABASE_NOT_FOUND = "Database does not exist."
3231

@@ -508,7 +507,7 @@ export class DatabaseImpl implements Database {
508507
return pouch.dump(stream, opts)
509508
}
510509

511-
async load(stream: Readable) {
510+
async load(stream: ReadStream) {
512511
const pouch = getPouchDB(this.name)
513512
// @ts-ignore
514513
return pouch.load(stream)

packages/backend-core/src/security/encryption.ts

Lines changed: 9 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import fs from "fs"
33
import zlib from "zlib"
44
import env from "../environment"
55
import { join } from "path"
6-
import { PassThrough, Readable } from "stream"
76

87
const ALGO = "aes-256-ctr"
98
const SEPARATOR = "-"
@@ -80,103 +79,24 @@ export async function encryptFile(
8079
const inputFile = fs.createReadStream(filePath)
8180
const outputFile = fs.createWriteStream(join(dir, outputFileName))
8281

83-
encryptStream(inputFile, secret).pipe(outputFile)
84-
85-
return new Promise<{ filename: string; dir: string }>((resolve, reject) => {
86-
outputFile.on("finish", () => {
87-
resolve({
88-
filename: outputFileName,
89-
dir,
90-
})
91-
})
92-
const cleanupReject = (error: Error) => {
93-
inputFile.close()
94-
outputFile.close()
95-
reject(error)
96-
}
97-
outputFile.on("error", cleanupReject)
98-
inputFile.on("error", cleanupReject)
99-
})
100-
}
101-
102-
export function encryptStream(inputStream: Readable, secret: string): Readable {
10382
const salt = crypto.randomBytes(SALT_LENGTH)
10483
const iv = crypto.randomBytes(IV_LENGTH)
10584
const stretched = stretchString(secret, salt)
10685
const cipher = crypto.createCipheriv(ALGO, stretched, iv)
107-
const gzip = zlib.createGzip()
108-
109-
const outputStream = new PassThrough()
110-
outputStream.write(salt)
111-
outputStream.write(iv)
112-
113-
// Set up error propagation
114-
inputStream.on("error", err => {
115-
gzip.destroy(err)
116-
if (!outputStream.destroyed) {
117-
outputStream.destroy(err)
118-
}
119-
})
120-
gzip.on("error", err => {
121-
cipher.destroy(err)
122-
if (!outputStream.destroyed) {
123-
outputStream.destroy(err)
124-
}
125-
})
126-
cipher.on("error", err => {
127-
if (!outputStream.destroyed) {
128-
outputStream.destroy(err)
129-
}
130-
})
13186

132-
inputStream.pipe(gzip).pipe(cipher).pipe(outputStream)
87+
outputFile.write(salt)
88+
outputFile.write(iv)
13389

134-
return outputStream
135-
}
90+
inputFile.pipe(zlib.createGzip()).pipe(cipher).pipe(outputFile)
13691

137-
export async function decryptStream(
138-
inputStream: Readable,
139-
secret: string
140-
): Promise<Readable> {
141-
const outputStream = new PassThrough()
142-
143-
let decipher: crypto.Decipher | null = null
144-
let gunzip: zlib.Gunzip | null = null
145-
let firstChunk = true
146-
147-
inputStream.on("data", chunk => {
148-
if (firstChunk) {
149-
firstChunk = false
150-
const salt = chunk.slice(0, SALT_LENGTH)
151-
const iv = chunk.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
152-
chunk = chunk.slice(SALT_LENGTH + IV_LENGTH)
153-
154-
const stretched = stretchString(secret, salt)
155-
decipher = crypto.createDecipheriv(ALGO, stretched, iv)
156-
gunzip = zlib.createGunzip()
157-
158-
inputStream.on("error", err => {
159-
decipher!.destroy(err)
160-
})
161-
decipher.on("error", err => {
162-
gunzip!.destroy(err)
163-
})
164-
gunzip.on("error", err => {
165-
outputStream.destroy(err)
92+
return new Promise<{ filename: string; dir: string }>(r => {
93+
outputFile.on("finish", () => {
94+
r({
95+
filename: outputFileName,
96+
dir,
16697
})
167-
168-
decipher.pipe(gunzip).pipe(outputStream)
169-
}
170-
171-
decipher!.write(chunk)
172-
})
173-
174-
inputStream.on("end", () => {
175-
decipher?.end()
176-
gunzip?.end()
98+
})
17799
})
178-
179-
return outputStream
180100
}
181101

182102
async function getSaltAndIV(path: string) {

packages/pro

Submodule pro updated from b4ad9b6 to 304cd59

packages/server/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
"snowflake-sdk": "^1.15.0",
123123
"socket.io": "4.8.1",
124124
"svelte": "4.2.19",
125-
"tar-stream": "3.1.7",
125+
"tar": "6.2.1",
126126
"tmp": "0.2.3",
127127
"to-json-schema": "0.2.5",
128128
"uuid": "^8.3.2",
@@ -155,7 +155,6 @@
155155
"@types/supertest": "2.0.14",
156156
"@types/swagger-jsdoc": "^6.0.4",
157157
"@types/tar": "6.1.5",
158-
"@types/tar-stream": "3.1.4",
159158
"@types/tmp": "0.2.6",
160159
"@types/uuid": "8.3.4",
161160
"chance": "^1.1.12",

packages/server/src/api/controllers/application.ts

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
deleteAppFiles,
1111
revertClientLibrary,
1212
updateClientLibrary,
13-
storeTempFileStream,
14-
downloadTemplate,
1513
} from "../../utilities/fileSystem"
1614
import {
1715
AppStatus,
@@ -29,7 +27,6 @@ import {
2927
env as envCore,
3028
events,
3129
features,
32-
HTTPError,
3330
objectStore,
3431
roles,
3532
tenancy,
@@ -84,8 +81,6 @@ import * as appMigrations from "../../appMigrations"
8481
import { createSampleDataTableScreen } from "../../constants/screens"
8582
import { defaultAppNavigator } from "../../constants/definitions"
8683
import { processMigrations } from "../../appMigrations/migrationsProcessor"
87-
import { ImportOpts } from "../../sdk/app/backups/imports"
88-
import { join } from "path"
8984

9085
// utility function, need to do away with this
9186
async function getLayouts() {
@@ -163,23 +158,11 @@ async function createInstance(appId: string, template: AppTemplate) {
163158
await createAllSearchIndex()
164159

165160
if (template && template.useTemplate) {
166-
const opts: ImportOpts = {
161+
const opts = {
167162
importObjStoreContents: true,
168163
updateAttachmentColumns: !template.key, // preserve attachments when using Budibase templates
169-
password: template.file?.password,
170164
}
171-
172-
let path = template.file?.path
173-
if (!path && template.key) {
174-
const [type, name] = template.key.split("/")
175-
const tmpPath = await downloadTemplate(type, name)
176-
path = join(tmpPath, name, "db", "dump.txt")
177-
}
178-
if (!path) {
179-
throw new HTTPError("App export must have path", 400)
180-
}
181-
182-
await sdk.backups.importApp(appId, db, path, opts)
165+
await sdk.backups.importApp(appId, db, template, opts)
183166
} else {
184167
// create the users table
185168
await db.put(USERS_TABLE_SCHEMA)
@@ -870,25 +853,23 @@ export async function importToApp(
870853
ctx: UserCtx<ImportToUpdateAppRequest, ImportToUpdateAppResponse>
871854
) {
872855
const { appId } = ctx.params
873-
874856
const appExport = ctx.request.files?.appExport
857+
const password = ctx.request.body.encryptionPassword
875858
if (!appExport) {
876859
ctx.throw(400, "Must supply app export to import")
877860
}
878861
if (Array.isArray(appExport)) {
879862
ctx.throw(400, "Must only supply one app export")
880863
}
881-
882-
if (!appExport.path) {
883-
ctx.throw(400, "App export must have path")
864+
const fileAttributes = { type: appExport.type!, path: appExport.path! }
865+
try {
866+
await sdk.applications.updateWithExport(appId, fileAttributes, password)
867+
} catch (err: any) {
868+
ctx.throw(
869+
500,
870+
`Unable to perform update, please retry - ${err?.message || err}`
871+
)
884872
}
885-
886-
await sdk.applications.updateWithExport(
887-
appId,
888-
appExport.path,
889-
ctx.request.body.encryptionPassword
890-
)
891-
892873
ctx.body = { message: "app updated" }
893874
}
894875

@@ -913,8 +894,10 @@ export async function duplicateApp(
913894
const url = sdk.applications.getAppUrl({ name: appName, url: possibleUrl })
914895
checkAppUrl(ctx, apps, url)
915896

916-
const stream = await sdk.backups.exportApp(sourceAppId)
917-
const tmpPath = await storeTempFileStream(stream)
897+
const tmpPath = await sdk.backups.exportApp(sourceAppId, {
898+
excludeRows: false,
899+
tar: false,
900+
})
918901

919902
const createRequestBody: CreateAppRequest = {
920903
name: appName,

packages/server/src/api/controllers/backup.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ export async function exportAppDump(
2626
const extension = encryptPassword ? "enc.tar.gz" : "tar.gz"
2727
const backupIdentifier = `${appName}-export-${new Date().getTime()}.${extension}`
2828
ctx.attachment(backupIdentifier)
29-
const stream = await sdk.backups.exportApp(appId, {
29+
ctx.body = await sdk.backups.streamExportApp({
30+
appId,
3031
excludeRows,
3132
encryptPassword,
3233
})
33-
stream.pipe(ctx.res)
34-
ctx.status = 200
3534

3635
await context.doInAppContext(appId, async () => {
3736
const appDb = context.getAppDB()

packages/server/src/api/controllers/plugin/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ export async function downloadUnzipTarball(
66
name: string,
77
headers = {}
88
) {
9-
const path = createTempFolder(name)
10-
await objectStore.downloadTarballDirect(url, path, headers)
11-
return path
9+
try {
10+
const path = createTempFolder(name)
11+
await objectStore.downloadTarballDirect(url, path, headers)
12+
13+
return path
14+
} catch (e: any) {
15+
throw new Error(e.message)
16+
}
1217
}

packages/server/src/api/routes/public/tests/applications.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe("check export/import", () => {
7373
expect(res.headers["content-disposition"]).toMatch(
7474
/attachment; filename=".*-export-.*\.tar.gz"/g
7575
)
76-
expect(res.body).toBeInstanceOf(Buffer)
76+
expect(res.body instanceof Buffer).toBe(true)
7777
expect(res.status).toBe(200)
7878
})
7979

packages/server/src/sdk/app/applications/import.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,20 +138,26 @@ async function getImportableDocuments(db: Database) {
138138

139139
export async function updateWithExport(
140140
appId: string,
141-
path: string,
141+
file: FileAttributes,
142142
password?: string
143143
) {
144144
const devId = dbCore.getDevAppID(appId)
145145
const tempAppName = `temp_${devId}`
146146
const tempDb = dbCore.getDB(tempAppName)
147147
const appDb = dbCore.getDB(devId)
148148
try {
149+
const template = {
150+
file: {
151+
type: file.type!,
152+
path: file.path!,
153+
password,
154+
},
155+
}
149156
// get a temporary version of the import
150157
// don't need obj store, the existing app already has everything we need
151-
await backups.importApp(devId, tempDb, path, {
158+
await backups.importApp(devId, tempDb, template, {
152159
importObjStoreContents: false,
153160
updateAttachmentColumns: true,
154-
password,
155161
})
156162
const newMetadata = await getNewAppMetadata(tempDb, appDb)
157163
// get the documents to copy

0 commit comments

Comments
 (0)