import { Effect, Context, Layer } from "effect"; import { execSync } from "child_process"; /** * Quota Management Service * * Manages disk quotas and resource limits for Linux users. * Provides storage limits to prevent individual users from * consuming excessive disk space. * * Features: * - Disk quota management (requires quota system on host) * - Directory size tracking * - Resource limit enforcement * - Usage monitoring */ export interface DiskQuota { softLimitMB: number; hardLimitMB: number; } export interface QuotaUsage { usedMB: number; softLimitMB: number; hardLimitMB: number; percentUsed: number; isOverSoftLimit: boolean; isOverHardLimit: boolean; } export interface QuotaError { readonly _tag: "QuotaError"; readonly message: string; readonly cause?: unknown; } const QuotaError = (message: string, cause?: unknown): QuotaError => ({ _tag: "QuotaError", message, cause, }); export class QuotaService extends Context.Tag("QuotaService")< QuotaService, { /** * Set disk quota for a user * Note: Requires quota support on the filesystem */ readonly setDiskQuota: ( username: string, quota: DiskQuota ) => Effect.Effect; /** * Get current quota usage for a user */ readonly getQuotaUsage: ( username: string ) => Effect.Effect; /** * Check if quota system is available */ readonly isQuotaAvailable: () => Effect.Effect; /** * Get directory size for a user (fallback if quota not available) */ readonly getDirectorySize: ( dirPath: string ) => Effect.Effect; /** * Enforce storage limits (alternative to disk quotas) */ readonly checkStorageLimit: ( username: string, homeDir: string, limitMB: number ) => Effect.Effect; } >() { static Live = Layer.succeed(this, { setDiskQuota: (username: string, quota: DiskQuota) => Effect.gen(function* () { // Check if quota is available const quotaAvailable = yield* Effect.tryPromise({ try: async () => { try { execSync("which setquota", { stdio: "ignore" }); return true; } catch { return false; } }, catch: (error) => QuotaError("Failed to check quota availability", error), }); if (!quotaAvailable) { return yield* Effect.fail( QuotaError( "Quota system not available. Install quota tools: sudo apt-get install quota" ) ); } // Set quota using setquota command // Format: setquota -u username soft_blocks hard_blocks soft_inodes hard_inodes filesystem const softBlocks = quota.softLimitMB * 1024; // Convert MB to blocks (1 block = 1KB) const hardBlocks = quota.hardLimitMB * 1024; const softInodes = 10000; // Reasonable inode limit const hardInodes = 15000; const filesystem = "/"; // Primary filesystem yield* Effect.tryPromise({ try: async () => { const command = `sudo setquota -u ${username} ${softBlocks} ${hardBlocks} ${softInodes} ${hardInodes} ${filesystem}`; execSync(command, { stdio: "pipe" }); }, catch: (error) => QuotaError(`Failed to set disk quota for ${username}`, error), }); }), getQuotaUsage: (username: string) => Effect.gen(function* () { // Check if quota is available const quotaAvailable = yield* Effect.tryPromise({ try: async () => { try { execSync("which quota", { stdio: "ignore" }); return true; } catch { return false; } }, catch: (error) => QuotaError("Failed to check quota availability", error), }); if (!quotaAvailable) { // Fallback: Calculate directory size const homeDir = `/home/${username}`; const usedMB = yield* Effect.tryPromise({ try: async () => { const output = execSync(`sudo du -sm ${homeDir}`, { encoding: "utf-8", }); return parseInt(output.split("\t")[0] || "0", 10); }, catch: (error) => QuotaError( `Failed to calculate directory size for ${username}`, error ), }); return { usedMB, softLimitMB: 1024, // Default 1GB soft limit hardLimitMB: 2048, // Default 2GB hard limit percentUsed: (usedMB / 2048) * 100, isOverSoftLimit: usedMB > 1024, isOverHardLimit: usedMB > 2048, }; } // Get quota usage using quota command const usage = yield* Effect.tryPromise({ try: async () => { const output = execSync(`sudo quota -u ${username} --show-mntpoint`, { encoding: "utf-8", }); // Parse quota output // Expected format: // Disk quotas for user username (uid 1001): // Filesystem blocks quota limit grace files quota limit grace // /dev/sda1 1024 1024000 2048000 100 10000 15000 const lines = output.trim().split("\n"); if (lines.length < 3) { throw new Error("Invalid quota output format"); } const dataLine = (lines[2] || "").trim().split(/\s+/); const usedBlocks = parseInt(dataLine[1] || "0", 10); const softBlocks = parseInt(dataLine[2] || "0", 10); const hardBlocks = parseInt(dataLine[3] || "0", 10); const usedMB = Math.round(usedBlocks / 1024); const softLimitMB = Math.round(softBlocks / 1024); const hardLimitMB = Math.round(hardBlocks / 1024); return { usedMB, softLimitMB, hardLimitMB, percentUsed: (usedMB / hardLimitMB) * 100, isOverSoftLimit: usedMB > softLimitMB, isOverHardLimit: usedMB > hardLimitMB, }; }, catch: (error) => QuotaError(`Failed to get quota usage for ${username}`, error), }); return usage; }), isQuotaAvailable: () => Effect.tryPromise({ try: async () => { try { execSync("which quota", { stdio: "ignore" }); execSync("which setquota", { stdio: "ignore" }); return true; } catch { return false; } }, catch: (error) => QuotaError("Failed to check quota availability", error), }), getDirectorySize: (dirPath: string) => Effect.tryPromise({ try: async () => { const output = execSync(`sudo du -sm ${dirPath}`, { encoding: "utf-8", }); return parseInt(output.split("\t")[0] || "0", 10); }, catch: (error) => QuotaError(`Failed to get directory size for ${dirPath}`, error), }), checkStorageLimit: (username: string, homeDir: string, limitMB: number) => Effect.gen(function* () { const usedMB = yield* Effect.tryPromise({ try: async () => { const output = execSync(`sudo du -sm ${homeDir}`, { encoding: "utf-8", }); return parseInt(output.split("\t")[0] || "0", 10); }, catch: (error) => QuotaError(`Failed to check storage for ${username}`, error), }); return usedMB <= limitMB; }), }); }