Skip to content

Commit

Permalink
better archival/delete handling
Browse files Browse the repository at this point in the history
  • Loading branch information
sdumetz committed Jul 3, 2024
1 parent a85b1d6 commit 140c83e
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 46 deletions.
40 changes: 40 additions & 0 deletions source/server/routes/api/v1/scenes/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,44 @@ describe("GET /api/v1/scenes", function(){
});
});

describe("archives", async function(){
let scenes:number[];
this.beforeAll(async ()=>{
scenes = [];
scenes.push(await vfs.createScene(`scene_archived`));
await vfs.archiveScene("scene_archived");
scenes.push(await vfs.createScene(`scene_live`));
});

this.afterAll(async function(){
await Promise.all(scenes.map(id=>vfs.removeScene(id)));
});

it("can get only archived scenes", async function(){
let r = await request(this.server).get("/api/v1/scenes?access=none")
.auth(admin.username, "12345678")
.expect(200);
let names = r.body.scenes.map((s:any)=>s.name)
expect(names).to.include(`scene_archived#${scenes[0].toString(10)}`);
});

it

it("requires global admin rights", async function(){
let r = await request(this.server).get("/api/v1/scenes?access=none")
.auth(user.username, "12345678")
.expect(200);
expect(r.body).to.have.property("scenes").to.have.length(0);
});

it("won't return archived scenes in a default query", async function(){
let r = await request(this.server).get("/api/v1/scenes")
.auth(user.username, "12345678")
.expect(200);
let names = r.body.scenes.map((s:any)=>s.name)
expect(names).not.to.include(`scene_archived#${scenes[0].toString(10)}`);
});

})

});
7 changes: 4 additions & 3 deletions source/server/routes/api/v1/scenes/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function getScenes(req :Request, res :Response){
orderDirection,
} = req.query;

access = ((Array.isArray(access))?access : (access?[access]:undefined)) as any;
let accessTypes :AccessType[] = ((Array.isArray(access))?access : (access?[access]:undefined)) as any;

let scenesList = [];
if(typeof ids === "string"){
Expand Down Expand Up @@ -54,12 +54,13 @@ export default async function getScenes(req :Request, res :Response){
scenes = await Promise.all(scenesList.map(name=>vfs.getScene(name)));
}else{
/**@fixme ugly hach to bypass permissions when not performing a search */
const requester_id = (u.isAdministrator && !access && !match)?undefined: u.uid;
const requester_id = (u.isAdministrator && (!accessTypes || (accessTypes.length == 1 && accessTypes[0] == "none")) && !match)?undefined: u.uid;

scenes = await vfs.getScenes(requester_id, {
match: match as string,
orderBy: orderBy as any,
orderDirection: orderDirection as any,
access: access as AccessType[],
access: accessTypes,
limit: limit? parseInt(limit as string): undefined,
offset: offset? parseInt(offset as string): undefined,
});
Expand Down
38 changes: 37 additions & 1 deletion source/server/routes/scenes/delete/scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { expect } from "chai";
import User from "../../../auth/User.js";
import UserManager from "../../../auth/UserManager.js";
import Vfs from "../../../vfs/index.js";
import { NotFoundError } from "../../../utils/errors.js";



Expand Down Expand Up @@ -35,6 +36,9 @@ describe("DELETE /scenes/:scene", function(){
await request(this.server).delete(`/scenes/${titleSlug}`)
.auth(user.username, "12345678")
.expect(204);

await request(this.server).get(`/scenes/${titleSlug}`)
.expect(404);
});

it("can delete as administrator", async function(){
Expand All @@ -59,7 +63,7 @@ describe("DELETE /scenes/:scene", function(){
.auth(user.username, "12345678")
.expect(204);

await request(this.server).mkcol(`/scenes/foo`)
await request(this.server).mkcol(`/scenes/${titleSlug}`)
.auth(user.username, "12345678")
.expect(201);
});
Expand All @@ -72,5 +76,37 @@ describe("DELETE /scenes/:scene", function(){
await request(this.server).delete(`/scenes/${titleSlug}`)
.auth(user.username, "12345678")
.expect(404);
});

it.skip("can restore an archived scene", async function(){


});

it("requires superadmin to force delete", async function(){

await request(this.server).delete(`/scenes/${titleSlug}?archive=false`)
.auth(user.username, "12345678")
.expect(401);
await expect(vfs.getScene(scene_id)).to.be.fulfilled;
});

it("can force delete a scene", async function(){
await request(this.server).delete(`/scenes/${titleSlug}?archive=false`)
.auth(admin.username, "12345678")
.expect(204);
await expect(vfs.getScene(scene_id)).to.be.rejectedWith(NotFoundError);
});

it("can force delete after archival", async function(){
await request(this.server).delete(`/scenes/${titleSlug}`)
.auth(admin.username, "12345678")
.expect(204);

await request(this.server).delete(`/scenes/${titleSlug}${encodeURIComponent("#")+scene_id.toString(10)}?archive=false`)
.auth(admin.username, "12345678")
.expect(204);

})

});
3 changes: 3 additions & 0 deletions source/server/routes/scenes/delete/scene.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

import { UnauthorizedError } from "../../../utils/errors.js";
import { getFileParams, getUser, getUserId, getUserManager, getVfs } from "../../../utils/locals.js";
import { Request, Response } from "express";

Expand All @@ -7,6 +8,8 @@ export default async function handleDeleteScene(req :Request, res :Response){
let user = getUser(req);
if(user.isAdministrator && req.query.archive === "false"){
await vfs.removeScene(req.params.scene);
}else if(req.query.archive === "false"){
throw new UnauthorizedError(`force-delete requires instance-level admin rights`);
}else{
await vfs.archiveScene(req.params.scene);
}
Expand Down
38 changes: 26 additions & 12 deletions source/server/vfs/Scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default abstract class ScenesVfs extends BaseVfs{

for(let i=0; i<3; i++){
try{
let uid = Uid.make();
//Unlikely, but still: skip uid that would prevent scene archiving
if(name.endsWith("#"+uid.toString(10))) continue;

let r = await this.db.get(`
INSERT INTO scenes (scene_name, scene_id, access)
VALUES (
Expand All @@ -31,7 +35,7 @@ export default abstract class ScenesVfs extends BaseVfs{
RETURNING scene_id AS scene_id;
`, {
$scene_name:name,
$scene_id: Uid.make(),
$scene_id: uid,
$access: JSON.stringify(permissions)
});
return r.scene_id;
Expand Down Expand Up @@ -61,14 +65,16 @@ export default abstract class ScenesVfs extends BaseVfs{
if(!r?.changes) throw new NotFoundError(`No scene found matching : ${scene}`);
}
/**
* set a scene access to "none" for everyone
* set a scene access to "none" for everyone, effectively making it hidden
* @see UserManager.grant for a more granular setup
*/
async archiveScene(scene :number|string){
let r = await this.db.run(`
UPDATE scenes
SET access = json_object('0', 'none')
WHERE ${typeof scene ==="number"? "scene_id": "scene_name"} = $scene
SET access = json_object('0', 'none'), scene_name = scene_name || '#' || scene_id
WHERE
${typeof scene ==="number"? "scene_id": "scene_name"} = $scene
${typeof scene ==="number"? `AND INSTR(scene_name, '#' || scene_id) = 0`:""}
`, {$scene: scene});
if(!r?.changes) throw new NotFoundError(`No scene found matching : ${scene}`);
}
Expand All @@ -83,10 +89,18 @@ export default abstract class ScenesVfs extends BaseVfs{
}

/**
* get all scenes when called without params
* Search scenes with structured queries when called with filters
* get all scenes, including archvied scenes Generally not used outside of tests and internal routines
*/
async getScenes():Promise<Scene[]>;
/**
* Get all scenes for <user_id>, filtering results with structured queries when called with filters
*/
async getScenes(user_id :number|undefined, q?:SceneQuery):Promise<Scene[]>;
/**
* Get only archived scenes.
*/
async getScenes(user_id ?:number, {access, match, limit =10, offset = 0, orderBy="name", orderDirection="asc"} :SceneQuery = {}) :Promise<Scene[]>{
async getScenes(user_id:null, q :{access:["none"]}) :Promise<Scene[]>;
async getScenes(user_id ?:number|null, {access, match, limit =10, offset = 0, orderBy="name", orderDirection="asc"} :SceneQuery = {}) :Promise<Scene[]>{

//Check various parameters compliance
if(Array.isArray(access) && access.find(a=>AccessTypes.indexOf(a) === -1)){
Expand All @@ -99,7 +113,7 @@ export default abstract class ScenesVfs extends BaseVfs{
if(["asc", "desc"].indexOf(orderDirection.toLowerCase()) === -1) throw new BadRequestError(`Invalid orderDirection: ${orderDirection}`);
if(["ctime", "mtime", "name"].indexOf(orderBy.toLowerCase()) === -1) throw new BadRequestError(`Invalid orderBy: ${orderBy}`);

let with_filter = typeof user_id === "number" || match;
let with_filter = typeof user_id === "number" || match || access?.length;

const sortString = (orderBy == "name")? "LOWER(scene_name)": orderBy;

Expand Down Expand Up @@ -187,10 +201,10 @@ export default abstract class ScenesVfs extends BaseVfs{
LEFT JOIN json_tree(document.metas) AS thumb ON thumb.fullkey LIKE "$[_].images[_]" AND json_extract(thumb.value, '$.quality') = 'Thumb'
${with_filter? "WHERE true": ""}
${typeof user_id === "number"? `AND
${(typeof user_id === "number")? `AND
COALESCE(
${(typeof user_id === "number")? `json_extract(scenes.access, '$.' || $user_id),` :""}
${(typeof user_id === "number" && 0 < user_id)? `json_extract(scenes.access, '$.1'),`:""}
json_extract(scenes.access, '$.' || $user_id),
${(0 < user_id)? `json_extract(scenes.access, '$.1'),`:""}
json_extract(scenes.access, '$.0')
) IN (${ AccessTypes.slice(2).map(s=>`'${s}'`).join(", ") })
`:""}
Expand All @@ -202,7 +216,7 @@ export default abstract class ScenesVfs extends BaseVfs{
LIMIT $offset, $limit
`, {
...mParams,
$user_id: user_id?.toString(10),
$user_id: (user_id? user_id.toString(10) : (access?.length? "0": undefined)),
$limit: Math.min(limit, 100),
$offset: offset,
})).map(({ctime, mtime, id, access, ...m})=>({
Expand Down
93 changes: 82 additions & 11 deletions source/server/vfs/vfs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,41 @@ describe("Vfs", function(){
await expect(vfs.createScene("foo")).to.be.fulfilled;
await expect(vfs.createScene("foo")).to.be.rejectedWith("exist");
});
it("retries for unused scene_id", async function(){
let old = Uid.make;
try{
Uid.make = ()=> 1;
await expect(vfs.createScene("bar")).to.be.fulfilled;
await expect(vfs.createScene("bar")).to.be.rejectedWith("Unable to find a free id");
}finally{

describe("uid handling", function(){
let old :typeof Uid.make;
let returns :number[] = [];
this.beforeEach(function(){
old = Uid.make;
returns = [];
Uid.make = ()=> {
let r = returns.pop();
if (typeof r === "undefined") throw new Error("No mock result provided");
return r;
};
});
this.afterEach(function(){
Uid.make = old;
}
});
});

it("fails if no free uid can be found", async function(){
returns = [1, 1, 1, 1];
await expect(vfs.createScene("bar")).to.be.fulfilled;
await expect(vfs.createScene("baz")).to.be.rejectedWith("Unable to find a free id");
});

it("retry", async function(){
returns = [1, 1, 2];
await expect(vfs.createScene("bar")).to.be.fulfilled;
await expect(vfs.createScene("baz")).to.be.fulfilled;
});

it("prevents scene name containing uid", async function(){
returns = [1, 2];
let scene_id = await expect(vfs.createScene("bar#1")).to.be.fulfilled;
expect(scene_id).to.equal(2);
});
})

it("sets scene author", async function(){
const userManager = new UserManager(vfs._db);
Expand Down Expand Up @@ -181,7 +206,30 @@ describe("Vfs", function(){
let s = await vfs.getScenes(0);
expect(s).to.have.property("length", 1);
expect(s[0]).to.have.property("thumb", "scene-image-thumb.jpg");
})
});

it("can get archived scenes", async function(){
let scene_id = await vfs.createScene("foo");
await vfs.writeDoc(JSON.stringify({foo: "bar"}), scene_id, 0);
await vfs.archiveScene(scene_id);
let scenes = await vfs.getScenes();
expect(scenes.map(({name})=>({name}))).to.deep.equal([{name: `foo#${scene_id}`}]);
});


it("can get only archived scenes", async function(){
await vfs.createScene("bar");
let scene_id = await vfs.createScene("foo");
await vfs.writeDoc(JSON.stringify({foo: "bar"}), scene_id, 0);
await vfs.archiveScene(scene_id);

//Two scenes total
expect(await vfs.getScenes()).to.have.length(2);
//Filter only scenes with access: none
let scenes = await vfs.getScenes(null, {access: ["none"]});
console.log(JSON.stringify(scenes));
expect(scenes.map(({name})=>({name}))).to.deep.equal([{name: `foo#${scene_id}`}]);
});

describe("with permissions", function(){
let userManager :UserManager, user :User;
Expand Down Expand Up @@ -468,10 +516,33 @@ describe("Vfs", function(){
})

describe("archiveScene()", function(){
it("set access rights to none", async function(){
it("makes scene hidden", async function(){
await vfs.archiveScene("foo");
expect(await vfs.getScenes(0)).to.have.property("length", 0);
});

it("changes scene name and access", async function(){
await vfs.archiveScene(scene_id);
const scenes = await vfs._db.all("SELECT * FROM scenes");
expect(scenes).to.have.length(1);
expect(scenes[0]).to.have.property("scene_name", `foo#${scene_id.toString(10)}`);
expect(scenes[0]).to.have.property("access", '{"0":"none"}');
});

it("can't archive twice", async function(){
await vfs.archiveScene(scene_id);
await expect(vfs.archiveScene(scene_id), (await vfs._db.all("SELECT * FROM scenes"))[0].scene_name).to.be.rejectedWith(NotFoundError);
});

it("can remove archived scene (by id)", async function(){
await vfs.archiveScene(scene_id);
await expect(vfs.removeScene(scene_id)).to.be.fulfilled;
});

it("can remove archived scene (by archived name)", async function(){
await vfs.archiveScene(scene_id);
await expect(vfs.removeScene(`foo#${scene_id.toString(10)}`)).to.be.fulfilled;
});
});

describe("createFile()", function(){
Expand Down
Loading

0 comments on commit 140c83e

Please sign in to comment.