diff --git a/docker-compose.worker.yml b/docker-compose.worker.yml index 1e1780d..67dcaa3 100644 --- a/docker-compose.worker.yml +++ b/docker-compose.worker.yml @@ -120,6 +120,15 @@ services: profiles: [capture] restart: unless-stopped runtime: nvidia + # Growing-files mode mounts an SMB/CIFS share inside the container + # (mount.cifs). That syscall needs CAP_SYS_ADMIN + DAC_READ_SEARCH and an + # unconfined AppArmor profile; without these the mount fails with + # "Unable to apply new capability set" and growing falls back to HEVC/S3. + cap_add: + - SYS_ADMIN + - DAC_READ_SEARCH + security_opt: + - apparmor:unconfined environment: REDIS_URL: ${REDIS_URL} DATABASE_URL: ${DATABASE_URL} diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 5e334ed..ffe40ba 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -64,13 +64,14 @@ function mountGrowingShare() { return true; } try { mkdirSync(GROWING_PATH, { recursive: true }); } catch (_) {} - writeFileSync( - SMB_CREDS_FILE, - `username=${GROWING_SMB_USERNAME}\npassword=${GROWING_SMB_PASSWORD}\n`, - { mode: 0o600 } - ); + // Pass credentials inline rather than via a credentials= file. Some SMB + // servers (notably TrueNAS SMB3) reject the credentials-file form with + // EACCES (-13) — "cannot mount ... read-only" — even though the very same + // username/password mount inline and smbclient lists the share fine. Inline + // user=/password= is the reliable form here. const opts = [ - `credentials=${SMB_CREDS_FILE}`, + `username=${GROWING_SMB_USERNAME}`, + `password=${GROWING_SMB_PASSWORD}`, 'uid=0', 'gid=0', 'file_mode=0664', 'dir_mode=0775', `vers=${GROWING_SMB_VERS}`, ].join(','); diff --git a/services/mam-api/src/routes/storage.js b/services/mam-api/src/routes/storage.js index d073061..8c365e5 100644 --- a/services/mam-api/src/routes/storage.js +++ b/services/mam-api/src/routes/storage.js @@ -78,14 +78,21 @@ async function probeS3Bucket() { if (!bucket) { out.error = 'no bucket configured'; return out; } const started = Date.now(); + // Hard cap the whole probe so the admin "Mount health" card never hangs on + // "Probing…" when S3 is slow/unreachable. Without this, the SDK's default + // retry/backoff can block the request for tens of seconds. + const withTimeout = (p, ms) => Promise.race([ + p, + new Promise((_, rej) => setTimeout(() => rej(new Error('probe timed out after ' + ms + 'ms')), ms)), + ]); try { - await s3Client.send(new HeadBucketCommand({ Bucket: bucket })); + await withTimeout(s3Client.send(new HeadBucketCommand({ Bucket: bucket })), 5000); out.reachable = true; out.method = 'HeadBucket'; } catch (headErr) { // Fall back to a 0-key list for stores that don't expose HeadBucket. try { - await s3Client.send(new ListObjectsV2Command({ Bucket: bucket, MaxKeys: 0 })); + await withTimeout(s3Client.send(new ListObjectsV2Command({ Bucket: bucket, MaxKeys: 0 })), 5000); out.reachable = true; out.method = 'ListObjectsV2'; } catch (listErr) {