Fix shared upload permissions for capability-restricted LTX backend

This commit is contained in:
Mikei386
2026-09-29 19:45:38 +02:00
parent 64573e01a3
commit 5b887dfb42
7 changed files with 41 additions and 7 deletions
+4 -3
View File
@@ -1,7 +1,7 @@
import http from 'node:http'
import https from 'node:https'
import { createReadStream, createWriteStream } from 'node:fs'
import { readFile, mkdir, realpath, stat, unlink, copyFile } from 'node:fs/promises'
import { readFile, mkdir, realpath, stat, unlink, copyFile, chmod } from 'node:fs/promises'
import path from 'node:path'
import { randomBytes, randomUUID, timingSafeEqual, createHash } from 'node:crypto'
import { pipeline } from 'node:stream/promises'
@@ -119,7 +119,7 @@ export async function createApp(config) {
if(Number(req.headers['content-length'])>limit)throw fail(413,'Upload too large')
const target=outputName(ext);let size=0
const limiter=new Transform({transform(chunk,_encoding,done){size+=chunk.length;done(size>limit?fail(413,'Upload too large'):null,chunk)}})
try {await pipeline(req,limiter,createWriteStream(target.local,{flags:'wx',mode:0o600}))} catch(e){await unlink(target.local).catch(()=>{});throw e}
try {await pipeline(req,limiter,createWriteStream(target.local,{flags:'wx',mode:0o640}));await chmod(target.local,0o640)} catch(e){await unlink(target.local).catch(()=>{});throw e}
return json(res,201,{success:true,path:target.backend})
}
if(url.pathname==='/web/files'&&req.method==='POST') {
@@ -131,7 +131,7 @@ export async function createApp(config) {
const source=await resolveMedia(p.path)
if(p.action==='prepare')return json(res,200,{success:true,path:p.path})
if(p.action==='copy') {
const target=outputName(path.extname(source));await copyFile(source,target.local);return json(res,200,{success:true,path:target.backend})
const target=outputName(path.extname(source));await copyFile(source,target.local);await chmod(target.local,0o640);return json(res,200,{success:true,path:target.backend})
}
if(p.action==='metadata'||p.action==='thumbnail'||p.action==='frame') {
if(p.action==='metadata') {
@@ -143,6 +143,7 @@ export async function createApp(config) {
if(!Number.isFinite(time)||time<0)throw fail(400,'Invalid frame time')
const target=outputName('.jpg')
await exec('ffmpeg',['-nostdin','-v','error','-ss',String(time),'-protocol_whitelist','file,pipe','-i',source,'-frames:v','1','-vf','scale=960:-2','-y',target.local],{timeout:60000,maxBuffer:1024*1024})
await chmod(target.local,0o640)
return json(res,200,{success:true,path:target.backend,bigThumbnailPath:target.backend,smallThumbnailPath:target.backend})
}
throw fail(400,'Unsupported file operation')
+5 -1
View File
@@ -1,7 +1,7 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import http from 'node:http'
import { mkdtemp, mkdir, writeFile, readFile, symlink, rm } from 'node:fs/promises'
import { mkdtemp, mkdir, writeFile, readFile, symlink, rm, stat, chmod } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { createApp } from './app.mjs'
@@ -54,6 +54,7 @@ test('upload, backend path, range playback and existence check',async t=>{
const {call,dir}=await fixture(t)
const upload=await call('/web/upload?name=clip.mp4',{method:'POST',body:'0123456789'})
assert.equal(upload.status,201);const result=await upload.json();assert.ok(result.path.startsWith('/ltx/inputs/'))
assert.equal((await stat(path.join(dir,'input',path.basename(result.path)))).mode & 0o777,0o640)
assert.equal(await readFile(path.join(dir,'input',path.basename(result.path)),'utf8'),'0123456789')
const media=await call('/web/media?path='+encodeURIComponent(result.path),{headers:{Range:'bytes=2-5'}})
assert.equal(media.status,206);assert.equal(media.headers.get('content-range'),'bytes 2-5/10');assert.equal(await media.text(),'2345')
@@ -76,13 +77,16 @@ test('shared media can be inspected, copied and thumbnailed without an inference
const {call,dir}=await fixture(t)
// Deliberately tiny synthetic PPM pixels; ffprobe detects content, not extension.
await writeFile(path.join(dir,'output','test.png'),Buffer.concat([Buffer.from('P6\n2 2\n255\n'),Buffer.from([0,0,255,0,0,255,0,0,255,0,0,255])]))
await chmod(path.join(dir,'output','test.png'),0o600)
const invoke=async action=>{
const response=await call('/web/files',{method:'POST',body:JSON.stringify({action,path:'/ltx/outputs/test.png'})})
assert.equal(response.status,200);return response.json()
}
const metadata=await invoke('metadata');assert.equal(metadata.width,2);assert.equal(metadata.height,2)
const copy=await invoke('copy');assert.ok(copy.path.startsWith('/ltx/inputs/'))
assert.equal((await stat(path.join(dir,'input',path.basename(copy.path)))).mode & 0o777,0o640)
const thumbnail=await invoke('thumbnail');assert.ok(thumbnail.path.endsWith('.jpg'))
assert.equal((await stat(path.join(dir,'input',path.basename(thumbnail.path)))).mode & 0o777,0o640)
const response=await call('/web/media?path='+encodeURIComponent(thumbnail.path));assert.equal(response.status,200)
assert.equal(response.headers.get('content-type'),'image/jpeg');assert.ok((await response.arrayBuffer()).byteLength>100)
})