# File Compress Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add single-file ZIP compression with password support and custom modal dialogs. **Architecture:** Backend adds `POST api/files/compress` using DotNetZip for encrypted ZIP. Frontend adds ConfirmModal + CompressModal components, replaces all system dialogs. **Tech Stack:** ASP.NET Core 10, DotNetZip, Vue 3.5, TypeScript 6 ## Global Constraints - Do not edit `FileShare-EFCore/Migrations/**` - All endpoints use POST (project convention) - Follow existing patterns: sealed record DTOs, ResponseHelper, Teleport modal pattern ## File Map | File | Action | Purpose | |------|--------|---------| | `FileShare-Services/FileShare-Services.csproj` | Modify | Add DotNetZip package | | `FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs` | Modify | Add CompressFileRequest DTO | | `FileShare-Services/Services/FileLibrary/IFileCompressionService.cs` | Create | Compression service interface | | `FileShare-Services/Services/FileLibrary/FileCompressionService.cs` | Create | Compression implementation | | `FileShare-Services/Services/FileLibrary/IFileCompressionEndpointService.cs` | Create | Endpoint interface | | `FileShare-Services/Services/FileLibrary/FileCompressionEndpointService.cs` | Create | Endpoint adapter | | `FileShare-Services/Endpoints/AppEndpoints.cs` | Modify | Register route | | `FileShare-API/Configuration/ServicesConfiguration.cs` | Modify | Register DI | | `FileShare-Web-VUE/src/api/index.ts` | Modify | Add compressFile API | | `FileShare-Web-VUE/src/components/ConfirmModal.vue` | Create | Generic confirm dialog | | `FileShare-Web-VUE/src/components/CompressModal.vue` | Create | Compress password dialog | | `FileShare-Web-VUE/src/components/client/FileCard.vue` | Modify | Add compress button | | `FileShare-Web-VUE/src/components/client/FileListItem.vue` | Modify | Add compress button | | `FileShare-Web-VUE/src/components/ClientPage.vue` | Modify | Handle compress + replace system dialogs | --- ## Task 1: Backend — Package, DTO, Service Interface - [ ] **Step 1: Add DotNetZip to FileShare-Services.csproj** Add to `` with other PackageReferences: ```xml ``` - [ ] **Step 2: Add CompressFileRequest to FileLibraryContracts.cs** After `DeleteFileRequest`, add: ```csharp /// /// 压缩文件的请求。 /// public sealed record CompressFileRequest( [property: JsonPropertyName("id")] int Id, [property: JsonPropertyName("password")] string? Password = null, [property: JsonPropertyName("useDefaultPassword")] bool UseDefaultPassword = true); ``` - [ ] **Step 3: Create IFileCompressionService.cs** ```csharp namespace FileShare_Services.Services.FileLibrary { public interface IFileCompressionService { Task CompressFileAsync(int id, string? password, bool useDefaultPassword, CancellationToken cancellationToken = default); } } ``` - [ ] **Step 4: Create IFileCompressionEndpointService.cs** ```csharp using FileShare_Common.Core; namespace FileShare_Services.Services.FileLibrary { public interface IFileCompressionEndpointService { Task CompressFileAsync(CompressFileRequest request); } } ``` --- ## Task 2: Backend — Service Implementation - [ ] **Step 1: Create FileCompressionService.cs** ```csharp using FileShare_EFCore.Database; using FileShare_EFCore.Models; using Microsoft.EntityFrameworkCore; using System.IO.Compression; namespace FileShare_Services.Services.FileLibrary { public sealed class FileCompressionService(AppDataContext db) : IFileCompressionService { public async Task CompressFileAsync(int id, string? password, bool useDefaultPassword, CancellationToken cancellationToken = default) { var record = await db.ManagedFileRecords .FirstOrDefaultAsync(f => f.Id == id && f.Exists, cancellationToken) ?? throw new InvalidOperationException("文件不存在。"); if (!File.Exists(record.AbsolutePath)) throw new FileNotFoundException("源文件不存在于磁盘。"); var dir = Path.GetDirectoryName(record.AbsolutePath)!; var nameWithoutExt = Path.GetFileNameWithoutExtension(record.FileName); var zipPath = Path.Combine(dir, nameWithoutExt + ".zip"); // 删除已存在的同名 ZIP if (File.Exists(zipPath)) { File.Delete(zipPath); } // 确定密码 string? zipPassword = ResolvePassword(record.FileName, password, useDefaultPassword); // 创建 ZIP using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) { archive.CreateEntryFromFile(record.AbsolutePath, record.FileName, CompressionLevel.Optimal); // DotNetZip 不支持 ZipArchive 密码,改用 DotNetZip 的 ZipFile } // 使用 DotNetZip 重新创建(支持密码) System.IO.Compression.ZipFile.Delete(zipPath); using (var zip = new Ionic.Zip.ZipFile()) { zip.Password = zipPassword; zip.AddFile(record.AbsolutePath, ""); zip.Save(zipPath); } } private static string? ResolvePassword(string fileName, string? password, bool useDefaultPassword) { if (useDefaultPassword) return Path.GetFileNameWithoutExtension(fileName); return string.IsNullOrEmpty(password) ? null : password; } } } ``` Wait — this mixes `System.IO.Compression` and `Ionic.Zip`. Let me use only DotNetZip. - [ ] **Step 1 (revised): Create FileCompressionService.cs** ```csharp using FileShare_EFCore.Database; using FileShare_EFCore.Models; using Ionic.Zip; using Microsoft.EntityFrameworkCore; namespace FileShare_Services.Services.FileLibrary { public sealed class FileCompressionService(AppDataContext db) : IFileCompressionService { public async Task CompressFileAsync(int id, string? password, bool useDefaultPassword, CancellationToken cancellationToken = default) { var record = await db.ManagedFileRecords .FirstOrDefaultAsync(f => f.Id == id && f.Exists, cancellationToken) ?? throw new InvalidOperationException("文件不存在。"); if (!File.Exists(record.AbsolutePath)) throw new FileNotFoundException("源文件不存在于磁盘。"); var dir = Path.GetDirectoryName(record.AbsolutePath)!; var nameWithoutExt = Path.GetFileNameWithoutExtension(record.FileName); var zipPath = Path.Combine(dir, nameWithoutExt + ".zip"); // 删除已存在的同名 ZIP if (File.Exists(zipPath)) { File.Delete(zipPath); } // 确定密码 var zipPassword = ResolvePassword(record.FileName, password, useDefaultPassword); // 创建 ZIP using var zip = new ZipFile(); if (zipPassword is not null) { zip.Password = zipPassword; } zip.AddFile(record.AbsolutePath, ""); zip.Save(zipPath); } private static string? ResolvePassword(string fileName, string? password, bool useDefaultPassword) { if (useDefaultPassword) return Path.GetFileNameWithoutExtension(fileName); return string.IsNullOrEmpty(password) ? null : password; } } } ``` --- ## Task 3: Backend — Endpoint + Route + DI - [ ] **Step 1: Create FileCompressionEndpointService.cs** ```csharp using FileShare_Common.Core; namespace FileShare_Services.Services.FileLibrary { public sealed class FileCompressionEndpointService(IFileCompressionService compressionService) : IFileCompressionEndpointService { public async Task CompressFileAsync(CompressFileRequest request) { if (request.Id <= 0) return ResponseHelper.Failure(400, "文件 ID 无效。"); await compressionService.CompressFileAsync(request.Id, request.Password, request.UseDefaultPassword); return ResponseHelper.Succeed("文件已压缩。"); } } } ``` - [ ] **Step 2: Register route in AppEndpoints.cs** After the `DeleteFile` endpoint, add: ```csharp endpoints.MapPost("api/files/compress", (service, request, _) => service.CompressFileAsync(request)) .WithOpenApi("FileLibrary", "压缩文件为 ZIP(可选密码)。") .WithName("CompressFile"); ``` - [ ] **Step 3: Register DI in ServicesConfiguration.cs** After `IFileLibraryEndpointService` registration, add: ```csharp services.AddScoped(); services.AddScoped(); ``` --- ## Task 4: Frontend — API + Modal Components - [ ] **Step 1: Add compressFile API method** In `api/index.ts`, add: ```typescript compressFile: (id: number, password?: string | null, useDefaultPassword = true) => request('files/compress', { method: 'POST', body: { id, password, useDefaultPassword } }), ``` - [ ] **Step 2: Create ConfirmModal.vue** ```vue ``` - [ ] **Step 3: Create CompressModal.vue** ```vue ``` --- ## Task 5: Frontend — Wire Components + Replace System Dialogs - [ ] **Step 1: Add compress buttons to FileCard.vue and FileListItem.vue** Add `compress` emit and button to both components (similar to delete button pattern). - [ ] **Step 2: Update ClientPage.vue** - Import ConfirmModal and CompressModal - Add refs for modal instances - Add `compressingIds` ref (Set tracking) - Replace `window.confirm` in `deleteFile` with ConfirmModal - Add `compressFile` handler using CompressModal - Wire `@compress` events on all FileCard/FileListItem instances - [ ] **Step 3: Verify build** ```bash dotnet build FileShare-API/FileShare-API.csproj cd FileShare-Web-VUE && npx vue-tsc --noEmit ```