fix(growing): inline CIFS creds + capture caps + storage probe timeout

Three fixes to restore growing-files (XDCAM HD422 MXF) recording:

1. capture-manager mountGrowingShare: pass username=/password= inline
   instead of a credentials= file. TrueNAS SMB3 rejects the creds-file form
   with EACCES (-13, 'cannot mount read-only') while the identical inline
   creds mount fine. This was causing every growing record to silently fall
   back to the HEVC/S3 path (producing .mov, not .mxf).

2. docker-compose capture: add cap_add SYS_ADMIN + DAC_READ_SEARCH and
   apparmor:unconfined so mount.cifs can run inside the container.

3. storage /overview: wrap S3 HeadBucket/ListObjects probe in a 5s timeout
   so the admin 'Mount health' card stops hanging on 'Probing…' forever
   when S3 is slow.
This commit is contained in:
Zac Gaetano 2026-06-04 12:42:39 +00:00
parent 2812705d1c
commit cb25711ec6
3 changed files with 25 additions and 8 deletions

View file

@ -120,6 +120,15 @@ services:
profiles: [capture] profiles: [capture]
restart: unless-stopped restart: unless-stopped
runtime: nvidia 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: environment:
REDIS_URL: ${REDIS_URL} REDIS_URL: ${REDIS_URL}
DATABASE_URL: ${DATABASE_URL} DATABASE_URL: ${DATABASE_URL}

View file

@ -64,13 +64,14 @@ function mountGrowingShare() {
return true; return true;
} }
try { mkdirSync(GROWING_PATH, { recursive: true }); } catch (_) {} try { mkdirSync(GROWING_PATH, { recursive: true }); } catch (_) {}
writeFileSync( // Pass credentials inline rather than via a credentials= file. Some SMB
SMB_CREDS_FILE, // servers (notably TrueNAS SMB3) reject the credentials-file form with
`username=${GROWING_SMB_USERNAME}\npassword=${GROWING_SMB_PASSWORD}\n`, // EACCES (-13) — "cannot mount ... read-only" — even though the very same
{ mode: 0o600 } // username/password mount inline and smbclient lists the share fine. Inline
); // user=/password= is the reliable form here.
const opts = [ const opts = [
`credentials=${SMB_CREDS_FILE}`, `username=${GROWING_SMB_USERNAME}`,
`password=${GROWING_SMB_PASSWORD}`,
'uid=0', 'gid=0', 'file_mode=0664', 'dir_mode=0775', 'uid=0', 'gid=0', 'file_mode=0664', 'dir_mode=0775',
`vers=${GROWING_SMB_VERS}`, `vers=${GROWING_SMB_VERS}`,
].join(','); ].join(',');

View file

@ -78,14 +78,21 @@ async function probeS3Bucket() {
if (!bucket) { out.error = 'no bucket configured'; return out; } if (!bucket) { out.error = 'no bucket configured'; return out; }
const started = Date.now(); 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 { try {
await s3Client.send(new HeadBucketCommand({ Bucket: bucket })); await withTimeout(s3Client.send(new HeadBucketCommand({ Bucket: bucket })), 5000);
out.reachable = true; out.reachable = true;
out.method = 'HeadBucket'; out.method = 'HeadBucket';
} catch (headErr) { } catch (headErr) {
// Fall back to a 0-key list for stores that don't expose HeadBucket. // Fall back to a 0-key list for stores that don't expose HeadBucket.
try { 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.reachable = true;
out.method = 'ListObjectsV2'; out.method = 'ListObjectsV2';
} catch (listErr) { } catch (listErr) {