From a9874908d23433c90a6cd0d27f6951cd984fcac4 Mon Sep 17 00:00:00 2001 From: Sebastien DUMETZ Date: Tue, 10 Dec 2024 14:22:27 +0100 Subject: [PATCH] update history restore route --- source/server/routes/history/diff/get.test.ts | 4 +- source/server/routes/history/post.test.ts | 1 - source/server/routes/history/post.ts | 84 +++++++++++-------- source/server/vfs/Files.ts | 4 +- 4 files changed, 56 insertions(+), 37 deletions(-) diff --git a/source/server/routes/history/diff/get.test.ts b/source/server/routes/history/diff/get.test.ts index 4de95042..69add293 100644 --- a/source/server/routes/history/diff/get.test.ts +++ b/source/server/routes/history/diff/get.test.ts @@ -34,7 +34,7 @@ describe("GET /history/:scene/:id/diff", function(){ .set("Accept", "text/plain") .expect(200) .expect("Content-Type", "text/plain; charset=utf-8"); - expect(res.text).to.equal(`1c1\n< Hello\n---\n> Hello World\n`); + expect(res.text).to.equal(`--- hello.txt\n+++ hello.txt\n@@ -1 +1 @@\n-Hello\n+Hello World\n`); }); it("get diff summary for documents", async function(){ @@ -44,7 +44,7 @@ describe("GET /history/:scene/:id/diff", function(){ .set("Accept", "application/json") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); - expect(res.body).to.have.property("diff", 'Diff of JSON data not yet supported'); + expect(res.body).to.have.property("diff").match(/Couldn't compile diff/); expect(res.body).to.have.property("src"); expect(res.body).to.have.property("dst"); }) diff --git a/source/server/routes/history/post.test.ts b/source/server/routes/history/post.test.ts index 71da51bd..bb0025fa 100644 --- a/source/server/routes/history/post.test.ts +++ b/source/server/routes/history/post.test.ts @@ -195,7 +195,6 @@ describe("POST /history/:scene", function(){ .send({name: "articles/hello.txt", generation: 1}) .expect("Content-Type", "application/json; charset=utf-8") .expect(400); - expect(res.text).to.match(/Trying to remove scene document for /); expect(await vfs.getDoc(scene_id)).to.have.property("data", `{"id": 1}`); }); diff --git a/source/server/routes/history/post.ts b/source/server/routes/history/post.ts index e616b4cf..87179af9 100644 --- a/source/server/routes/history/post.ts +++ b/source/server/routes/history/post.ts @@ -15,48 +15,66 @@ import { HistoryEntry, ItemEntry } from "../../vfs/index.js"; * @see {getSceneHistory} */ export async function postSceneHistory(req :Request, res :Response){ + // Keep in mind history is in reverse-natural order, with newest files coming first. + // This makes everything "backward" from natural order let requester = getUser(req); let {scene:sceneName} = req.params; - let {name, generation } = req.body; - let files :Map = new Map(); - if(!(typeof name === "string" && typeof generation === "number")){ - throw new BadRequestError(`History restoration requires either of "name" and "generation" or "id" and "type" or "name" to be set`); + let {name, generation, id } = req.body; + /**keep a reference to the name and generation of the files that are to be undone */ + let files :Map = new Map(); + if(!(typeof name === "string" && typeof generation === "number") && typeof id !== "number"){ + throw new BadRequestError(`History restoration requires either "name" and "generation" or "id" to be set`); } - await getVfs(req).isolate(async (tr)=>{ - - let scene = await tr.getScene(sceneName); - let history = await tr.getSceneHistory(scene.id); - - /* Find index of the history entry we are restoring to */ - let index = history.findIndex((item)=> { - return (item.name == name && item.generation == generation) - }); - if(index === -1) throw new BadRequestError(`No file found in ${sceneName} matching ${(name+"#"+generation)}`); - - // Keep in mind history is in reverse-natural order, with newest files coming first. - //Slice history to everything *after* index. That's every refs that was registered *before* cutoff. - let refs = history.slice(index); - //Keep a reference of files that will be modified: Every ref that is *before* index. - files = new Map(history.slice(0, index).map(item=>([`${item.name}`, item]))); + await getVfs(req).isolate(async (vfs)=>{ + let scene = await vfs.getScene(sceneName); + let index :number = -1, offset = 0; + while(index < 0){ + let historySlice = await vfs.getSceneHistory(scene.id, {limit: 100, offset}); + for(let idx = 0; idx < historySlice.length; idx++){ + const item = historySlice[idx]; + if(((typeof id === "number")? (item.id === id) : (item.name == name && item.generation == generation))){ + index = idx + offset; + break; + } + files.set(item.name, item.generation); + } + if(historySlice.length < 100) break; + } + if(index === -1) throw new BadRequestError(`No file found in ${sceneName} matching ${typeof id =="number"? "id:"+id: (name+"#"+generation)}`); - for(let file of files.values()){ - //Find which version of the file needs to be restored : - let prev = refs.find((ref)=> ref.name === file.name); - if(file.mime !== "application/si-dpo-3d.document+json"){ - let theFile = (prev? await tr.getFileById(prev.id): {hash: null, size: 0}); - await tr.createFile({scene: scene.id, name: file.name, user_id: (prev? prev.author_id : requester.uid) }, theFile ) - }else if(typeof prev === "undefined"){ - throw new BadRequestError(`Trying to remove scene document for ${sceneName}. This would create an invalid scene`); - }else{ - let {data} = await tr.getFileById(prev.id); - await tr.writeDoc(data ?? null, { + for (let [name, generation] of files.entries()){ + let prev =( (1 < generation)? (await vfs.getFileProps({scene: scene.id, name, generation: generation - 1, archive: true}, true)): null); + if(prev?.data){ + await vfs.writeDoc(prev.data ?? null, { + scene: scene.id, + user_id: prev.author_id, + name: name, + mime: prev.mime, + }); + }else if(prev){ + await vfs.createFile({ scene: scene.id, + name: name, user_id: prev.author_id, - name: "scene.svx.json", - mime: "application/si-dpo-3d.document+json", + }, { + hash: prev.hash, + size: prev.size, + mime: prev.mime, + }); + }else if(name==="scene.svx.json"){ + throw new BadRequestError("Deleting a scene's document entirely is forbidden"); + }else{ + await vfs.createFile({ + scene: scene.id, + name: name, + user_id: requester.uid, + }, { + hash: null, + size: 0, }); } + } }); diff --git a/source/server/vfs/Files.ts b/source/server/vfs/Files.ts index 1f9c2518..fafd97c7 100644 --- a/source/server/vfs/Files.ts +++ b/source/server/vfs/Files.ts @@ -332,7 +332,9 @@ export default abstract class FilesVfs extends BaseVfs{ ctime: BaseVfs.toDate(r.ctime), //z specifies the string as UTC != localtime })); } - + /** + * a shortHand to createFile(params, {hash: null, size: 0}) that also verifies if the file actually exists + */ async removeFile(params :WriteFileParams) :Promise{ return await this.isolate(async function(tr){ //Check if file does exist