mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add configurable file/dir perms and UMASK support
Introduce file and directory permission settings (fileChmod, dirChmod) end-to-end. UI: new controls in Paths settings with octal validation and defaults (664/775). API: GET exposes defaults; PUT validates octal strings and upserts configuration keys (file_chmod, dir_chmod) and clears related cache keys. Runtime: read config values in file utilities and services (FileOrganizer, direct-download, chapter-merger, epub-fixer) to apply mkdir modes and chmod files/dirs; FileOrganizer now accepts fileMode/dirMode and getFileOrganizer reads/parses DB settings. Docker: add UMASK option to docker-compose and propagate/apply UMASK in entrypoint/app-start scripts. Tests: update mocks to account for config service usage.
This commit is contained in:
@@ -49,6 +49,15 @@ services:
|
|||||||
PUID: 1000
|
PUID: 1000
|
||||||
PGID: 1000
|
PGID: 1000
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# OPTIONAL: File Permission Mask
|
||||||
|
# ========================================================================
|
||||||
|
# Set a umask to control default file permissions for all files created
|
||||||
|
# by the application. Common values:
|
||||||
|
# - 002: Group-writable (files: 664, dirs: 775) - recommended for shared access
|
||||||
|
# - 022: Group-readable only (files: 644, dirs: 755) - more restrictive
|
||||||
|
# UMASK: "002"
|
||||||
|
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
# OPTIONAL: Secrets (auto-generated on first run if not provided)
|
# OPTIONAL: Secrets (auto-generated on first run if not provided)
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ PGID=${PGID:-$(id -g node)}
|
|||||||
echo "[App] Starting Next.js server..."
|
echo "[App] Starting Next.js server..."
|
||||||
echo "[App] Process will run as UID:GID = $PUID:$PGID"
|
echo "[App] Process will run as UID:GID = $PUID:$PGID"
|
||||||
|
|
||||||
|
# Apply UMASK if set (controls default file permissions)
|
||||||
|
if [ -n "$UMASK" ]; then
|
||||||
|
echo "[App] Applying umask: $UMASK"
|
||||||
|
umask "$UMASK"
|
||||||
|
fi
|
||||||
|
|
||||||
cd /app
|
cd /app
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ PORT=$PORT
|
|||||||
HOSTNAME=$HOSTNAME
|
HOSTNAME=$HOSTNAME
|
||||||
PUID=${PUID:-}
|
PUID=${PUID:-}
|
||||||
PGID=${PGID:-}
|
PGID=${PGID:-}
|
||||||
|
UMASK=${UMASK:-}
|
||||||
ROOTLESS_CONTAINER=${ROOTLESS_CONTAINER:-}
|
ROOTLESS_CONTAINER=${ROOTLESS_CONTAINER:-}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ export interface PathsSettings {
|
|||||||
chapterMergingEnabled: boolean;
|
chapterMergingEnabled: boolean;
|
||||||
fileRenameEnabled: boolean;
|
fileRenameEnabled: boolean;
|
||||||
fileRenameTemplate?: string;
|
fileRenameTemplate?: string;
|
||||||
|
fileChmod?: string;
|
||||||
|
dirChmod?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -439,6 +439,54 @@ export function PathsTab({ paths, onChange, onValidationChange }: PathsTabProps)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* File Permissions */}
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
File Permissions
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
Octal permissions applied when organizing files into the media library. These may be further restricted by the container's UMASK setting.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
File Permissions
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={paths.fileChmod || '664'}
|
||||||
|
onChange={(e) => updatePath('fileChmod', e.target.value)}
|
||||||
|
placeholder="664"
|
||||||
|
className={`font-mono max-w-32 ${paths.fileChmod && !/^[0-7]{3,4}$/.test(paths.fileChmod) ? 'border-red-500 dark:border-red-500' : ''}`}
|
||||||
|
/>
|
||||||
|
{paths.fileChmod && !/^[0-7]{3,4}$/.test(paths.fileChmod) && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">Must be 3-4 octal digits (0-7)</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
e.g. 664 = owner/group read-write, others read
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Directory Permissions
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={paths.dirChmod || '775'}
|
||||||
|
onChange={(e) => updatePath('dirChmod', e.target.value)}
|
||||||
|
placeholder="775"
|
||||||
|
className={`font-mono max-w-32 ${paths.dirChmod && !/^[0-7]{3,4}$/.test(paths.dirChmod) ? 'border-red-500 dark:border-red-500' : ''}`}
|
||||||
|
/>
|
||||||
|
{paths.dirChmod && !/^[0-7]{3,4}$/.test(paths.dirChmod) && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">Must be 3-4 octal digits (0-7)</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
e.g. 775 = owner/group full access, others read-execute
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Test Paths Button */}
|
{/* Test Paths Button */}
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||||
return requireAdmin(req, async () => {
|
return requireAdmin(req, async () => {
|
||||||
try {
|
try {
|
||||||
const { downloadDir, mediaDir, audiobookPathTemplate, ebookPathTemplate, metadataTaggingEnabled, chapterMergingEnabled, fileRenameEnabled, fileRenameTemplate } = await request.json();
|
const { downloadDir, mediaDir, audiobookPathTemplate, ebookPathTemplate, metadataTaggingEnabled, chapterMergingEnabled, fileRenameEnabled, fileRenameTemplate, fileChmod, dirChmod } = await request.json();
|
||||||
|
|
||||||
if (!downloadDir || !mediaDir) {
|
if (!downloadDir || !mediaDir) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -32,6 +32,21 @@ export async function PUT(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate octal permission strings (3-4 digits, each 0-7)
|
||||||
|
const octalRegex = /^[0-7]{3,4}$/;
|
||||||
|
if (fileChmod !== undefined && !octalRegex.test(fileChmod)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File permissions must be 3-4 octal digits (0-7), e.g. 664' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (dirChmod !== undefined && !octalRegex.test(dirChmod)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Directory permissions must be 3-4 octal digits (0-7), e.g. 775' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update configuration
|
// Update configuration
|
||||||
await prisma.configuration.upsert({
|
await prisma.configuration.upsert({
|
||||||
where: { key: 'download_dir' },
|
where: { key: 'download_dir' },
|
||||||
@@ -123,6 +138,34 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update file permissions (octal chmod)
|
||||||
|
if (fileChmod !== undefined) {
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'file_chmod' },
|
||||||
|
update: { value: fileChmod },
|
||||||
|
create: {
|
||||||
|
key: 'file_chmod',
|
||||||
|
value: fileChmod,
|
||||||
|
category: 'automation',
|
||||||
|
description: 'Octal permissions applied to organized files',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update directory permissions (octal chmod)
|
||||||
|
if (dirChmod !== undefined) {
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'dir_chmod' },
|
||||||
|
update: { value: dirChmod },
|
||||||
|
create: {
|
||||||
|
key: 'dir_chmod',
|
||||||
|
value: dirChmod,
|
||||||
|
category: 'automation',
|
||||||
|
description: 'Octal permissions applied to created directories',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Paths settings updated');
|
logger.info('Paths settings updated');
|
||||||
|
|
||||||
// Clear config cache for all updated keys so services get fresh values
|
// Clear config cache for all updated keys so services get fresh values
|
||||||
@@ -135,6 +178,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
configService.clearCache('chapter_merging_enabled');
|
configService.clearCache('chapter_merging_enabled');
|
||||||
configService.clearCache('file_rename_enabled');
|
configService.clearCache('file_rename_enabled');
|
||||||
configService.clearCache('file_rename_template');
|
configService.clearCache('file_rename_template');
|
||||||
|
configService.clearCache('file_chmod');
|
||||||
|
configService.clearCache('dir_chmod');
|
||||||
|
|
||||||
// Invalidate all download client singletons to force reload of download_dir
|
// Invalidate all download client singletons to force reload of download_dir
|
||||||
const { invalidateDownloadClientManager } = await import('@/lib/services/download-client-manager.service');
|
const { invalidateDownloadClientManager } = await import('@/lib/services/download-client-manager.service');
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export async function GET(request: NextRequest) {
|
|||||||
chapterMergingEnabled: configMap.get('chapter_merging_enabled') === 'true',
|
chapterMergingEnabled: configMap.get('chapter_merging_enabled') === 'true',
|
||||||
fileRenameEnabled: configMap.get('file_rename_enabled') === 'true',
|
fileRenameEnabled: configMap.get('file_rename_enabled') === 'true',
|
||||||
fileRenameTemplate: configMap.get('file_rename_template') || '{title}',
|
fileRenameTemplate: configMap.get('file_rename_template') || '{title}',
|
||||||
|
fileChmod: configMap.get('file_chmod') || '664',
|
||||||
|
dirChmod: configMap.get('dir_chmod') || '775',
|
||||||
},
|
},
|
||||||
ebook: {
|
ebook: {
|
||||||
// New granular source toggles (with migration from legacy ebook_sidecar_enabled)
|
// New granular source toggles (with migration from legacy ebook_sidecar_enabled)
|
||||||
|
|||||||
@@ -289,8 +289,11 @@ async function downloadFileWithProgress(
|
|||||||
logger: RMABLogger
|
logger: RMABLogger
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Ensure target directory exists
|
// Ensure target directory exists with configured permissions
|
||||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
const configService = getConfigService();
|
||||||
|
const dirChmodStr = await configService.get('dir_chmod') || '775';
|
||||||
|
const dirMode = parseInt(dirChmodStr, 8);
|
||||||
|
await fs.mkdir(path.dirname(targetPath), { recursive: true, mode: dirMode });
|
||||||
|
|
||||||
// Start download with axios streaming
|
// Start download with axios streaming
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface MergeOptions {
|
|||||||
year?: number;
|
year?: number;
|
||||||
asin?: string;
|
asin?: string;
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
|
dirMode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MergeResult {
|
export interface MergeResult {
|
||||||
@@ -616,7 +617,7 @@ export async function mergeChapters(
|
|||||||
await logger?.info(`✓ All ${chapters.length} source files validated`);
|
await logger?.info(`✓ All ${chapters.length} source files validated`);
|
||||||
|
|
||||||
// Ensure temp directory exists
|
// Ensure temp directory exists
|
||||||
await fs.mkdir(tempDir, { recursive: true });
|
await fs.mkdir(tempDir, { recursive: true, ...(options.dirMode !== undefined && { mode: options.dirMode }) });
|
||||||
|
|
||||||
// Create concat file
|
// Create concat file
|
||||||
const concatContent = chapters
|
const concatContent = chapters
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import * as cheerio from 'cheerio';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { RMABLogger } from './logger';
|
import { RMABLogger } from './logger';
|
||||||
|
import { getConfigService } from '../services/config.service';
|
||||||
|
|
||||||
const moduleLogger = RMABLogger.create('EpubFixer');
|
const moduleLogger = RMABLogger.create('EpubFixer');
|
||||||
|
|
||||||
@@ -204,7 +205,10 @@ export async function fixEpubForKindle(
|
|||||||
// Create unique temp subdirectory to avoid filename conflicts
|
// Create unique temp subdirectory to avoid filename conflicts
|
||||||
// This preserves the original filename for the final organized file
|
// This preserves the original filename for the final organized file
|
||||||
const uniqueDir = path.join(tempDir, `kindle-fix-${Date.now()}`);
|
const uniqueDir = path.join(tempDir, `kindle-fix-${Date.now()}`);
|
||||||
await fs.mkdir(uniqueDir, { recursive: true });
|
const configService = getConfigService();
|
||||||
|
const dirChmodStr = await configService.get('dir_chmod') || '775';
|
||||||
|
const dirMode = parseInt(dirChmodStr, 8);
|
||||||
|
await fs.mkdir(uniqueDir, { recursive: true, mode: dirMode });
|
||||||
|
|
||||||
// Keep original filename
|
// Keep original filename
|
||||||
const sourceFilename = path.basename(sourcePath);
|
const sourceFilename = path.basename(sourcePath);
|
||||||
|
|||||||
@@ -64,10 +64,14 @@ export interface LoggerConfig {
|
|||||||
export class FileOrganizer {
|
export class FileOrganizer {
|
||||||
private mediaDir: string;
|
private mediaDir: string;
|
||||||
private tempDir: string;
|
private tempDir: string;
|
||||||
|
private fileMode: number;
|
||||||
|
private dirMode: number;
|
||||||
|
|
||||||
constructor(mediaDir: string = '/media/audiobooks', tempDir: string = '/tmp/readmeabook') {
|
constructor(mediaDir: string = '/media/audiobooks', tempDir: string = '/tmp/readmeabook', fileMode: number = 0o664, dirMode: number = 0o775) {
|
||||||
this.mediaDir = mediaDir;
|
this.mediaDir = mediaDir;
|
||||||
this.tempDir = tempDir;
|
this.tempDir = tempDir;
|
||||||
|
this.fileMode = fileMode;
|
||||||
|
this.dirMode = dirMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,6 +170,7 @@ export class FileOrganizer {
|
|||||||
year: audiobook.year,
|
year: audiobook.year,
|
||||||
asin: audiobook.asin,
|
asin: audiobook.asin,
|
||||||
outputPath,
|
outputPath,
|
||||||
|
dirMode: this.dirMode,
|
||||||
},
|
},
|
||||||
logger ?? undefined
|
logger ?? undefined
|
||||||
);
|
);
|
||||||
@@ -293,7 +298,7 @@ export class FileOrganizer {
|
|||||||
await logger?.info(`Target path: ${targetPath}`);
|
await logger?.info(`Target path: ${targetPath}`);
|
||||||
|
|
||||||
// Create target directory
|
// Create target directory
|
||||||
await fs.mkdir(targetPath, { recursive: true });
|
await fs.mkdir(targetPath, { recursive: true, mode: this.dirMode });
|
||||||
|
|
||||||
// Determine if file renaming should be applied
|
// Determine if file renaming should be applied
|
||||||
const shouldRename = renameConfig?.enabled && renameConfig.template;
|
const shouldRename = renameConfig?.enabled && renameConfig.template;
|
||||||
@@ -386,7 +391,7 @@ export class FileOrganizer {
|
|||||||
// Copy file via streams (avoids copy_file_range EPERM on NFS/FUSE)
|
// Copy file via streams (avoids copy_file_range EPERM on NFS/FUSE)
|
||||||
await copyFile(sourcePath, targetFilePath);
|
await copyFile(sourcePath, targetFilePath);
|
||||||
// Set explicit permissions after copy
|
// Set explicit permissions after copy
|
||||||
await fs.chmod(targetFilePath, 0o644);
|
await fs.chmod(targetFilePath, this.fileMode);
|
||||||
|
|
||||||
result.audioFiles.push(targetFilePath);
|
result.audioFiles.push(targetFilePath);
|
||||||
result.filesMovedCount++;
|
result.filesMovedCount++;
|
||||||
@@ -422,7 +427,7 @@ export class FileOrganizer {
|
|||||||
try {
|
try {
|
||||||
await fs.access(originalSourcePath, fs.constants.R_OK);
|
await fs.access(originalSourcePath, fs.constants.R_OK);
|
||||||
await copyFile(originalSourcePath, targetFilePath);
|
await copyFile(originalSourcePath, targetFilePath);
|
||||||
await fs.chmod(targetFilePath, 0o644);
|
await fs.chmod(targetFilePath, this.fileMode);
|
||||||
result.audioFiles.push(targetFilePath);
|
result.audioFiles.push(targetFilePath);
|
||||||
result.filesMovedCount++;
|
result.filesMovedCount++;
|
||||||
await logger?.info(`Fallback copy succeeded (without metadata tags): ${filename}`);
|
await logger?.info(`Fallback copy succeeded (without metadata tags): ${filename}`);
|
||||||
@@ -457,7 +462,7 @@ export class FileOrganizer {
|
|||||||
try {
|
try {
|
||||||
// Copy cover art (do NOT delete original)
|
// Copy cover art (do NOT delete original)
|
||||||
await copyFile(sourcePath, targetCoverPath);
|
await copyFile(sourcePath, targetCoverPath);
|
||||||
await fs.chmod(targetCoverPath, 0o644);
|
await fs.chmod(targetCoverPath, this.fileMode);
|
||||||
result.coverArtFile = targetCoverPath;
|
result.coverArtFile = targetCoverPath;
|
||||||
result.filesMovedCount++;
|
result.filesMovedCount++;
|
||||||
await logger?.info(`Copied cover art`);
|
await logger?.info(`Copied cover art`);
|
||||||
@@ -718,7 +723,7 @@ export class FileOrganizer {
|
|||||||
|
|
||||||
// Copy from local cache instead of downloading
|
// Copy from local cache instead of downloading
|
||||||
await copyFile(cachedPath, targetPath);
|
await copyFile(cachedPath, targetPath);
|
||||||
await fs.chmod(targetPath, 0o644);
|
await fs.chmod(targetPath, this.fileMode);
|
||||||
moduleLogger.debug(`Copied cover art from cache: ${filename}`);
|
moduleLogger.debug(`Copied cover art from cache: ${filename}`);
|
||||||
} else {
|
} else {
|
||||||
// Download from external URL (e.g., Audible CDN)
|
// Download from external URL (e.g., Audible CDN)
|
||||||
@@ -846,7 +851,7 @@ export class FileOrganizer {
|
|||||||
await logger?.info(`Target directory: ${targetDir}`);
|
await logger?.info(`Target directory: ${targetDir}`);
|
||||||
|
|
||||||
// Create target directory
|
// Create target directory
|
||||||
await fs.mkdir(targetDir, { recursive: true });
|
await fs.mkdir(targetDir, { recursive: true, mode: this.dirMode });
|
||||||
|
|
||||||
// Build target filename (apply rename template if enabled, otherwise sanitize source filename)
|
// Build target filename (apply rename template if enabled, otherwise sanitize source filename)
|
||||||
const sourceFilename = path.basename(ebookFile);
|
const sourceFilename = path.basename(ebookFile);
|
||||||
@@ -882,7 +887,7 @@ export class FileOrganizer {
|
|||||||
|
|
||||||
// Copy ebook file (do NOT delete original - may need for seeding or retry)
|
// Copy ebook file (do NOT delete original - may need for seeding or retry)
|
||||||
await copyFile(sourceFilePath, targetPath);
|
await copyFile(sourceFilePath, targetPath);
|
||||||
await fs.chmod(targetPath, 0o644);
|
await fs.chmod(targetPath, this.fileMode);
|
||||||
|
|
||||||
await logger?.info(`Copied ebook: ${targetFilename}`);
|
await logger?.info(`Copied ebook: ${targetFilename}`);
|
||||||
|
|
||||||
@@ -968,7 +973,7 @@ export class FileOrganizer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get FileOrganizer instance configured from database settings
|
* Get FileOrganizer instance configured from database settings
|
||||||
* Reads media_dir from database configuration, falls back to /media/audiobooks if not configured
|
* Reads media_dir, file_chmod, dir_chmod from database configuration
|
||||||
*/
|
*/
|
||||||
export async function getFileOrganizer(): Promise<FileOrganizer> {
|
export async function getFileOrganizer(): Promise<FileOrganizer> {
|
||||||
// Read media_dir from database config
|
// Read media_dir from database config
|
||||||
@@ -979,7 +984,15 @@ export async function getFileOrganizer(): Promise<FileOrganizer> {
|
|||||||
const mediaDir = config?.value || process.env.MEDIA_DIR || '/media/audiobooks';
|
const mediaDir = config?.value || process.env.MEDIA_DIR || '/media/audiobooks';
|
||||||
const tempDir = process.env.TEMP_DIR || '/tmp/readmeabook';
|
const tempDir = process.env.TEMP_DIR || '/tmp/readmeabook';
|
||||||
|
|
||||||
return new FileOrganizer(mediaDir, tempDir);
|
// Read file/directory permission settings
|
||||||
|
const { getConfigService } = await import('../services/config.service');
|
||||||
|
const configService = getConfigService();
|
||||||
|
const fileChmodStr = await configService.get('file_chmod') || '664';
|
||||||
|
const dirChmodStr = await configService.get('dir_chmod') || '775';
|
||||||
|
const fileMode = parseInt(fileChmodStr, 8);
|
||||||
|
const dirMode = parseInt(dirChmodStr, 8);
|
||||||
|
|
||||||
|
return new FileOrganizer(mediaDir, tempDir, fileMode, dirMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import { createPrismaMock } from '../helpers/prisma';
|
|||||||
import { createJobQueueMock } from '../helpers/job-queue';
|
import { createJobQueueMock } from '../helpers/job-queue';
|
||||||
|
|
||||||
const prismaMock = createPrismaMock();
|
const prismaMock = createPrismaMock();
|
||||||
const configMock = vi.hoisted(() => ({ get: vi.fn() }));
|
const configMock = vi.hoisted(() => ({
|
||||||
|
get: vi.fn(),
|
||||||
|
getMany: vi.fn().mockResolvedValue({ prowlarr_api_key: null }),
|
||||||
|
}));
|
||||||
const jobQueueMock = createJobQueueMock();
|
const jobQueueMock = createJobQueueMock();
|
||||||
const qbtMock = vi.hoisted(() => ({ addTorrent: vi.fn() }));
|
const qbtMock = vi.hoisted(() => ({ addTorrent: vi.fn() }));
|
||||||
const sabMock = vi.hoisted(() => ({ addNZB: vi.fn() }));
|
const sabMock = vi.hoisted(() => ({ addNZB: vi.fn() }));
|
||||||
@@ -54,6 +57,8 @@ vi.mock('@/lib/integrations/prowlarr.service', () => ({
|
|||||||
describe('processDownloadTorrent', () => {
|
describe('processDownloadTorrent', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
// Restore default implementations cleared by clearAllMocks
|
||||||
|
configMock.getMany.mockResolvedValue({ prowlarr_api_key: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
const torrentPayload = {
|
const torrentPayload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user