Files_Implementation_Plan.md
35,9 KB • MD • jun. 18, 2026 06:54
# Files — Implementation Plan for a .NET 10 MVC Razor Web File Browser
Generated: 2026-06-11
## 1. Project summary
**Project name:** `Files`
`Files` is an ASP.NET Core MVC Razor web application that turns the application's `wwwroot` folder into a rich web-based file browser, editor, viewer, uploader, and soft-delete manager.
The application has **no login system** and **no user model**. Anonymous visitors can browse, upload, create folders, open supported media in-browser, edit supported text files, download files, and delete files. User interface settings are stored in the browser using `localStorage`.
The visual result must match the images in the repository's `Designs` folder as closely as possible. All UI, controls, dialogs, upload states, context menus, empty states, media players, file cards, thumbnail frames, and unsupported-file views should use one unified theme system.
## 2. Current platform target
Use the latest .NET 10 SDK available on the target machine and target `net10.0`.
Recommended start command:
```bash
dotnet new mvc -n Files -f net10.0
cd Files
```
Use ASP.NET Core MVC with Razor views, Razor partials, and view components. Do **not** build this as a SPA. JavaScript should enhance the Razor-rendered UI, not replace it.
Official references checked while preparing this document:
- .NET downloads: https://dotnet.microsoft.com/en-us/download
- .NET support policy: https://dotnet.microsoft.com/en-us/platform/support/policy
- ASP.NET Core MVC overview: https://learn.microsoft.com/en-us/aspnet/core/mvc/overview?view=aspnetcore-10.0
- ASP.NET Core static files: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-10.0
- ASP.NET Core file uploads: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-10.0
- ASP.NET Core partial views: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-10.0
- ASP.NET Core view components: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-10.0
- ASP.NET Core antiforgery: https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-10.0
- Playwright .NET: https://playwright.dev/dotnet/docs/intro
- Playwright browser install: https://playwright.dev/dotnet/docs/browsers
- File icon vectors: https://github.com/dmhendricks/file-icon-vectors
## 3. Non-negotiable requirements
1. Project name is `Files`.
2. Target framework is `.NET 10` / `net10.0`.
3. UI architecture is ASP.NET Core MVC + Razor views/partials/view components.
4. The browser root is the app's `wwwroot` folder.
5. Deleted files are moved to a sibling folder outside `wwwroot` named `Deleted`.
6. Anonymous visitors can read, upload/write, create folders, edit supported text files, download, and delete.
7. There is no login, user account, role system, or database-backed profile.
8. Settings are stored in `localStorage`.
9. The implementation must follow the `Designs` folder images closely.
10. Playwright tests must verify the file browser works and displays correctly.
11. Rich previews must exist for images, videos, PDFs, audio where practical, and text/code files.
12. Unsupported files still get a details/download/delete experience.
13. File-type support must be modular so more viewers and thumbnail renderers can be added later.
14. Non-rich icons must use the `file-icon-vectors` Vivid icon set, adapted to the theme.
15. Global theme colors must be centralized so the design can be changed later without rewriting components.
## 4. Important safety constraint
Anonymous upload and delete against `wwwroot` is inherently risky. Since `wwwroot` is publicly addressable by ASP.NET Core static file middleware, uploaded files can become directly reachable by URL. The requested behavior is acceptable for a trusted/internal tool, a local network tool, a development utility, or a deliberately public drop zone, but the implementation still needs guardrails.
Minimum guardrails:
- Normalize and validate every path.
- Block path traversal.
- Never expose the `Deleted` folder through static file middleware.
- Protect application asset folders from deletion.
- Limit upload size.
- Sanitize file and folder names.
- Use antiforgery tokens even though there is no login.
- Block or neutralize dangerous extensions if the app will ever be internet-facing.
- Disable execution permissions on the file storage location at the host/OS level where possible.
Because the requirement says `wwwroot` is the shown file root, the app should treat `wwwroot` as the storage root but protect internal app folders such as CSS, JS, vendor assets, icons, and design references.
Recommended protected paths:
```json
{
"FileStorage": {
"RootPath": "wwwroot",
"DeletedPath": "Deleted",
"ProtectedPaths": [
"css",
"js",
"lib",
"vendor",
"assets",
"icons",
"Designs",
"favicon.ico"
],
"MaxUploadBytes": 524288000
}
}
```
If the app assets can be moved out of the browsed `wwwroot`, do it. If they cannot, protected paths are mandatory.
## 5. Recommended solution structure
```text
Files/
Files.csproj
Program.cs
appsettings.json
Controllers/
BrowserController.cs
FileApiController.cs
Models/
FileItem.cs
FolderListing.cs
FileActionResult.cs
FilePreviewRequest.cs
FileStorageOptions.cs
ViewerContext.cs
ThumbnailContext.cs
Services/
FileStorage/
IFileStorageService.cs
FileStorageService.cs
PathSafety.cs
MimeTypeService.cs
DeletedFileService.cs
Modules/
IFileModule.cs
IFileModuleRegistry.cs
FileModuleRegistry.cs
FileModuleDescriptor.cs
Modules/
Default/
DefaultFileModule.cs
Images/
ImageFileModule.cs
Video/
VideoFileModule.cs
Audio/
AudioFileModule.cs
Pdf/
PdfFileModule.cs
Text/
TextFileModule.cs
ViewComponents/
FileCardViewComponent.cs
FileViewerViewComponent.cs
FileThumbnailViewComponent.cs
BreadcrumbViewComponent.cs
Views/
Browser/
Index.cshtml
Shared/
_Layout.cshtml
_ThemeHead.cshtml
_ValidationScriptsPartial.cshtml
Components/
FileCard/Default.cshtml
FileViewer/Default.cshtml
FileThumbnail/Default.cshtml
Breadcrumb/Default.cshtml
Partials/
_Toolbar.cshtml
_Breadcrumbs.cshtml
_Grid.cshtml
_List.cshtml
_FileCard.cshtml
_UploadDialog.cshtml
_CreateFolderDialog.cshtml
_DeleteConfirmDialog.cshtml
_InspectorPanel.cshtml
_EmptyState.cshtml
FileModules/
Default/
_Viewer.cshtml
_Thumbnail.cshtml
_Details.cshtml
Images/
_Viewer.cshtml
_Thumbnail.cshtml
Video/
_Viewer.cshtml
_Thumbnail.cshtml
Audio/
_Viewer.cshtml
_Thumbnail.cshtml
Pdf/
_Viewer.cshtml
_Thumbnail.cshtml
Text/
_Viewer.cshtml
_Thumbnail.cshtml
_Editor.cshtml
wwwroot/
css/
files.tokens.css
files.layout.css
files.components.css
files.media.css
files.icons.css
js/
files.browser.js
files.localstorage.js
files.upload.js
files.viewer.js
files.editor.js
vendor/
file-icon-vectors/
dist/
Designs/
[design images, reference only]
Deleted/
[soft-deleted files, not web-served]
tests/
Files.PlaywrightTests/
Files.PlaywrightTests.csproj
BrowserSmokeTests.cs
FileOperationsTests.cs
RichViewerTests.cs
VisualRegressionTests.cs
```
## 6. MVC routing and endpoints
Use normal MVC routes for the UI and JSON endpoints for actions.
Recommended UI routes:
```text
GET / -> BrowserController.Index(path = "")
GET /browse -> BrowserController.Index(path)
GET /browse/view -> BrowserController.View(path)
```
Recommended API routes:
```text
GET /api/fs/list?path=...
GET /api/fs/raw?path=...
GET /api/fs/download?path=...
GET /api/fs/thumbnail?path=...
POST /api/fs/upload
POST /api/fs/folder
POST /api/fs/save-text
DELETE /api/fs/delete?path=...
```
Use `raw` for rich browser previews. It should set the correct content type and enable range processing for large media.
Example controller return for playable media:
```csharp
return PhysicalFile(
physicalPath,
contentType,
fileDownloadName: null,
enableRangeProcessing: true);
```
Use `download` for explicit downloads. It should force `Content-Disposition: attachment` by passing a download file name:
```csharp
return PhysicalFile(
physicalPath,
contentType,
fileDownloadName: Path.GetFileName(physicalPath),
enableRangeProcessing: true);
```
## 7. Program.cs baseline
```csharp
using Microsoft.AspNetCore.Mvc;
using Files.Models;
using Files.Services.FileStorage;
using Files.Services.Modules;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<FileStorageOptions>(
builder.Configuration.GetSection("FileStorage"));
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
builder.Services.AddSingleton<IFileStorageService, FileStorageService>();
builder.Services.AddSingleton<IMimeTypeService, MimeTypeService>();
builder.Services.AddSingleton<IDeletedFileService, DeletedFileService>();
builder.Services.AddSingleton<IFileModuleRegistry, FileModuleRegistry>();
// Register modules explicitly so priority and behavior are deterministic.
builder.Services.AddSingleton<IFileModule, ImageFileModule>();
builder.Services.AddSingleton<IFileModule, VideoFileModule>();
builder.Services.AddSingleton<IFileModule, AudioFileModule>();
builder.Services.AddSingleton<IFileModule, PdfFileModule>();
builder.Services.AddSingleton<IFileModule, TextFileModule>();
builder.Services.AddSingleton<IFileModule, DefaultFileModule>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseHttpsRedirection();
// Serve normal static assets from wwwroot. Be careful: this also means uploaded files are reachable.
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Browser}/{action=Index}/{id?}");
app.Run();
```
## 8. Path safety implementation
Every file action must go through one path safety service. Do not combine user input with physical paths directly inside controllers.
Rules:
- Treat incoming paths as relative paths only.
- Reject absolute paths.
- Reject paths with `..` after normalization.
- Reject invalid filename characters.
- Canonicalize separators to `/` in UI and to the OS separator for disk operations.
- Resolve the full physical path and verify it remains inside `wwwroot`.
- Reject actions against protected folders and files.
- Keep `Deleted` outside the static web root.
Example core helper:
```csharp
public sealed class PathSafety
{
private readonly string _rootFullPath;
private readonly HashSet<string> _protectedTopLevel;
public PathSafety(string rootFullPath, IEnumerable<string> protectedPaths)
{
_rootFullPath = Path.GetFullPath(rootFullPath)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
_protectedTopLevel = protectedPaths
.Select(p => p.Trim('/', '\\'))
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
}
public string ToSafeFullPath(string? relativePath)
{
relativePath ??= string.Empty;
if (Path.IsPathRooted(relativePath))
throw new InvalidOperationException("Absolute paths are not allowed.");
var normalizedRelative = relativePath
.Replace('\\', '/')
.TrimStart('/');
var fullPath = Path.GetFullPath(Path.Combine(_rootFullPath, normalizedRelative));
if (!fullPath.StartsWith(_rootFullPath, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Path traversal is not allowed.");
EnsureNotProtected(normalizedRelative);
return fullPath;
}
public void EnsureNotProtected(string normalizedRelative)
{
var firstSegment = normalizedRelative
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault();
if (firstSegment is not null && _protectedTopLevel.Contains(firstSegment))
throw new InvalidOperationException($"'{firstSegment}' is protected.");
}
}
```
## 9. File storage service responsibilities
Create `IFileStorageService` as the only disk access layer used by controllers and modules.
Recommended methods:
```csharp
public interface IFileStorageService
{
Task<FolderListing> ListAsync(string? path, CancellationToken ct);
Task<FileItem> GetAsync(string path, CancellationToken ct);
Task<string> SaveUploadAsync(string targetFolder, IFormFile file, CancellationToken ct);
Task CreateFolderAsync(string parentPath, string folderName, CancellationToken ct);
Task SaveTextAsync(string path, string content, CancellationToken ct);
Task MoveToDeletedAsync(string path, CancellationToken ct);
string GetPhysicalPath(string path);
string GetRelativeUrl(string path);
}
```
File listing should return both folders and files with the metadata required by the UI:
```csharp
public sealed record FileItem(
string Name,
string RelativePath,
bool IsDirectory,
long? SizeBytes,
string? Extension,
string? ContentType,
DateTimeOffset ModifiedAt,
bool CanPreview,
bool CanEdit,
bool CanDownload,
bool CanDelete,
string ModuleKey);
```
Sort folders before files by default, but allow localStorage preferences to override:
- Name ascending/descending
- Modified date ascending/descending
- Type
- Size
- Folders first on/off
## 10. Deleted folder behavior
Deleted files and folders must move outside `wwwroot` to `Deleted`.
Recommended delete target format:
```text
Deleted/
2026/
06/
11/
20260611-143522-9f4c2a/
original-relative-path/
file.ext
delete-info.json
```
`delete-info.json` should include:
```json
{
"deletedAtUtc": "2026-06-11T12:35:22Z",
"originalRelativePath": "uploads/video.mp4",
"originalName": "video.mp4",
"originalSizeBytes": 12345678,
"deletedBy": "anonymous",
"restoreNote": "Manual restore: copy the file or folder back to wwwroot at originalRelativePath."
}
```
This preserves enough information for manual restore without building a restore UI.
## 11. File module architecture
The file browser should not hard-code media handling inside the controller or main view. Use modules for file-type behavior.
### 11.1 Module interface
```csharp
public interface IFileModule
{
string Key { get; }
int Priority { get; }
IReadOnlySet<string> Extensions { get; }
bool CanHandle(FileItem item);
FileModuleDescriptor Describe(FileItem item);
}
```
```csharp
public sealed record FileModuleDescriptor(
string Key,
bool SupportsInlineViewer,
bool SupportsThumbnail,
bool SupportsEditing,
string ViewerPartial,
string ThumbnailPartial,
string DetailsPartial);
```
### 11.2 Registry
```csharp
public interface IFileModuleRegistry
{
IFileModule Resolve(FileItem item);
FileModuleDescriptor Describe(FileItem item);
}
```
```csharp
public sealed class FileModuleRegistry : IFileModuleRegistry
{
private readonly IReadOnlyList<IFileModule> _modules;
public FileModuleRegistry(IEnumerable<IFileModule> modules)
{
_modules = modules
.OrderByDescending(m => m.Priority)
.ToList();
}
public IFileModule Resolve(FileItem item)
=> _modules.First(m => m.CanHandle(item));
public FileModuleDescriptor Describe(FileItem item)
=> Resolve(item).Describe(item);
}
```
### 11.3 Built-in modules
| Module | Extensions | Viewer | Thumbnail | Editor |
|---|---|---|---|---|
| `Images` | `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`, `.avif`, `.svg` | `<img>`/zoom/pan | actual image | no |
| `Video` | `.mp4`, `.webm`, `.mov`, `.m4v`, `.ogv` | `<video controls>` | generated or browser poster | no |
| `Audio` | `.mp3`, `.wav`, `.ogg`, `.m4a`, `.flac` | `<audio controls>` | themed audio tile | no |
| `Pdf` | `.pdf` | `<iframe>` or `<object>` | themed PDF card or server thumbnail later | no |
| `Text` | `.txt`, `.md`, `.json`, `.xml`, `.csv`, `.log`, `.css`, `.js`, `.html`, `.cs`, `.cshtml` | syntax/plain reader | themed text card | yes, configurable |
| `Default` | all other files | details/download/delete | Vivid file icon | no |
For text editing, use a simple first version with `<textarea>` plus save. Later, Monaco Editor can be added as a module enhancement.
## 12. Razor composition pattern
Use one stable shell and delegate file-specific output to partials.
Main browser view:
```razor
@model FolderListing
<div class="files-shell" data-files-root>
<partial name="Partials/_Toolbar" model="Model" />
<partial name="Partials/_Breadcrumbs" model="Model" />
<main class="files-main">
<section class="files-content" data-view-mode="grid">
<partial name="Partials/_Grid" model="Model" />
</section>
<aside class="files-inspector" data-inspector hidden>
<partial name="Partials/_InspectorPanel" />
</aside>
</main>
<partial name="Partials/_UploadDialog" />
<partial name="Partials/_CreateFolderDialog" />
<partial name="Partials/_DeleteConfirmDialog" />
</div>
```
File card partial delegates thumbnail rendering:
```razor
@model FileItem
<article class="file-card" data-path="@Model.RelativePath" data-module="@Model.ModuleKey">
<a class="file-card__open" href="/browse/view?path=@Uri.EscapeDataString(Model.RelativePath)">
@await Component.InvokeAsync("FileThumbnail", new { item = Model })
<span class="file-card__name">@Model.Name</span>
</a>
<menu class="file-card__actions">
<a href="/api/fs/download?path=@Uri.EscapeDataString(Model.RelativePath)">Download</a>
<button type="button" data-delete="@Model.RelativePath">Delete</button>
</menu>
</article>
```
Viewer component chooses module-specific partial:
```csharp
public sealed class FileViewerViewComponent : ViewComponent
{
private readonly IFileModuleRegistry _modules;
public FileViewerViewComponent(IFileModuleRegistry modules)
{
_modules = modules;
}
public IViewComponentResult Invoke(FileItem item)
{
var descriptor = _modules.Describe(item);
var model = new ViewerContext(item, descriptor);
return View(descriptor.ViewerPartial, model);
}
}
```
Example image viewer partial:
```razor
@model ViewerContext
<figure class="viewer viewer--image">
<img
class="viewer__image"
src="/api/fs/raw?path=@Uri.EscapeDataString(Model.Item.RelativePath)"
alt="@Model.Item.Name"
loading="eager" />
<figcaption class="viewer__caption">
<span>@Model.Item.Name</span>
<a class="button" href="/api/fs/download?path=@Uri.EscapeDataString(Model.Item.RelativePath)">Download</a>
</figcaption>
</figure>
```
Example video viewer partial:
```razor
@model ViewerContext
<section class="viewer viewer--video">
<video
class="viewer__video themed-media-control"
src="/api/fs/raw?path=@Uri.EscapeDataString(Model.Item.RelativePath)"
controls
playsinline
preload="metadata">
</video>
<div class="viewer__actions">
<a class="button" href="/api/fs/download?path=@Uri.EscapeDataString(Model.Item.RelativePath)">Download</a>
<button class="button button--danger" data-delete="@Model.Item.RelativePath">Delete</button>
</div>
</section>
```
## 13. Theme system
Create a token-first CSS system. Do not scatter hard-coded colors across components.
Files:
```text
wwwroot/css/files.tokens.css
wwwroot/css/files.layout.css
wwwroot/css/files.components.css
wwwroot/css/files.media.css
wwwroot/css/files.icons.css
```
`files.tokens.css` should be the main design contract:
```css
:root {
/* Replace placeholder values with sampled values from Designs/*.png or Designs/*.jpg. */
--files-bg: #0d1117;
--files-surface: #151b23;
--files-surface-raised: #1d2633;
--files-border: rgba(255, 255, 255, 0.12);
--files-border-strong: rgba(255, 255, 255, 0.22);
--files-text: #f3f6fb;
--files-text-muted: #aab4c2;
--files-accent: #7aa2ff;
--files-accent-2: #9b7aff;
--files-danger: #ff5f6d;
--files-warning: #ffc857;
--files-success: #4fd18b;
--files-radius-xs: 6px;
--files-radius-sm: 10px;
--files-radius-md: 16px;
--files-radius-lg: 24px;
--files-shadow-card: 0 18px 50px rgba(0, 0, 0, 0.32);
--files-shadow-popover: 0 24px 70px rgba(0, 0, 0, 0.45);
--files-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--files-font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
--files-card-width: 168px;
--files-card-thumb-height: 128px;
--files-sidebar-width: 300px;
--files-toolbar-height: 64px;
}
```
After inspecting the design images, replace the placeholder colors with exact sampled theme values.
### 13.1 Design matching process
1. Put all provided design images in `Designs/`.
2. Create a design audit file: `Designs/design-audit.md`.
3. For each image, record:
- screen name
- viewport size
- dominant background color
- surface color
- card color
- border color
- accent color
- text color
- font family/weight if identifiable
- border radii
- spacing rhythm
- shadows/glows
- icon style
4. Translate findings into `files.tokens.css`.
5. Build components only using tokens.
6. Add Playwright screenshots for comparison.
7. Use a visual diff threshold; adjust CSS until matching the designs closely.
### 13.2 Styling native media controls
Browser media controls are limited in how much they can be restyled. The first implementation should still theme the media container, poster area, captions, buttons, progress surroundings, and action bar.
Use native controls for reliability:
```html
<video controls playsinline preload="metadata"></video>
<audio controls preload="metadata"></audio>
```
Then style surrounding UI:
```css
.viewer--video,
.viewer--audio {
background: linear-gradient(180deg, var(--files-surface-raised), var(--files-surface));
border: 1px solid var(--files-border);
border-radius: var(--files-radius-lg);
box-shadow: var(--files-shadow-card);
overflow: hidden;
}
.viewer__video,
.viewer__audio {
width: 100%;
accent-color: var(--files-accent);
background: #000;
}
```
If exact custom controls are required later, create a `VideoControls` partial with JavaScript calling `HTMLMediaElement.play()`, `pause()`, `currentTime`, `volume`, `requestFullscreen()`, and `textTracks`. Keep native controls available as a fallback.
## 14. LocalStorage settings
Use localStorage for browser-only preferences. Never store secrets.
Key:
```text
files:settings:v1
```
Suggested shape:
```json
{
"viewMode": "grid",
"sortBy": "name",
"sortDirection": "asc",
"foldersFirst": true,
"density": "comfortable",
"thumbnailSize": "large",
"showInspector": true,
"lastPath": "uploads/photos",
"sidebarWidth": 320,
"theme": "design-default"
}
```
Small settings module:
```javascript
const settingsKey = "files:settings:v1";
export function loadSettings() {
try {
return JSON.parse(localStorage.getItem(settingsKey) || "{}");
} catch {
return {};
}
}
export function saveSettings(patch) {
const next = { ...loadSettings(), ...patch };
localStorage.setItem(settingsKey, JSON.stringify(next));
return next;
}
```
## 15. Uploads and folder creation
Upload UX should support:
- toolbar upload button
- drag-and-drop onto folder/grid
- multiple files
- progress state
- conflict handling
- clear failure messages
- max-size validation
- accepted/unaccepted file feedback
Conflict handling options:
1. overwrite existing file
2. rename new file as `name (1).ext`
3. cancel
Default should be rename, because anonymous overwrite can destroy content unexpectedly.
Folder creation rules:
- Trim whitespace.
- Reject empty names.
- Reject path separators.
- Reject reserved names.
- Reject names that differ only by trailing dots/spaces on Windows-hosted deployments.
- If a folder exists, show an error; do not silently merge.
## 16. Rich thumbnail rendering
Thumbnail rendering should be modular and progressive.
### 16.1 First implementation
- Folders: large themed folder tile.
- Images: direct `<img>` thumbnail using `/api/fs/raw`.
- Videos: `<video preload="metadata">` tile with play badge, or generic video icon if expensive.
- PDFs: Vivid PDF icon in themed card; optional first-page thumbnail later.
- Audio: Vivid audio icon plus waveform-like themed decoration.
- Text/code: Vivid icon plus first lines preview if file is small.
- Unknown files: Vivid extension icon, fallback blank icon.
### 16.2 Later server thumbnails
Add `IThumbnailProvider` only after the browser version works.
```csharp
public interface IThumbnailProvider
{
bool CanGenerate(FileItem item);
Task<ThumbnailResult> GetOrCreateAsync(FileItem item, CancellationToken ct);
}
```
Possible later providers:
- Image resizing via ImageSharp or built-in decoding library.
- Video poster frame via FFmpeg, if allowed in the deployment environment.
- PDF first page via a PDF renderer.
Keep generated thumbnails in a cache outside the public browsed content or inside a protected cache folder.
## 17. File icons
Use `dmhendricks/file-icon-vectors`, specifically the Vivid set.
Install option:
```bash
npm install file-icon-vectors
```
Then copy the needed `dist` files into:
```text
wwwroot/vendor/file-icon-vectors/dist/
```
Reference the Vivid CSS:
```html
<link rel="stylesheet" href="~/vendor/file-icon-vectors/dist/file-icon-vivid.min.css" />
```
Example icon markup:
```html
<span class="fiv-viv fiv-icon-pdf file-icon file-icon--themed"></span>
```
The library documents the Vivid prefix as `fiv-viv` and extension classes as `fiv-icon-[extension]`.
### 17.1 Theme adaptation
Do not modify third-party vendor files directly. Instead:
1. Wrap every icon with `.file-icon-frame`.
2. Apply theme background, border, shadow, and hover treatment to the frame.
3. Use CSS filters only if they produce acceptable results.
4. For exact color matching, add a build step that copies selected SVGs into `wwwroot/icons/vivid-themed/` and rewrites known palette colors to CSS variables.
5. Keep attribution/license notes in `NOTICE.md`.
Example wrapper:
```css
.file-icon-frame {
display: grid;
place-items: center;
width: 72px;
height: 72px;
border-radius: var(--files-radius-md);
background: color-mix(in srgb, var(--files-surface-raised), var(--files-accent) 8%);
border: 1px solid var(--files-border);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}
.file-icon-frame .fiv-viv {
font-size: 2.8rem;
filter: saturate(0.85) brightness(1.08);
}
```
## 18. Supported and unsupported file behavior
### Supported inline viewer
When a supported file is clicked:
1. Open `/browse/view?path=...`.
2. Resolve the module.
3. Render the module viewer partial.
4. Provide actions: Download, Delete, Open raw, Copy path.
5. Add keyboard support: Escape/back, Delete key with confirm, arrow navigation through siblings.
### Unsupported file
When an unsupported file is clicked:
1. Show a details view using the `Default` module.
2. Show filename, extension, size, modified date, relative path, content type if known.
3. Primary action: Download.
4. Secondary actions: Delete, Copy path.
5. Do not attempt inline rendering.
## 19. Text editor module
The Text module should start simple and safe.
Supported file types:
```text
.txt, .md, .json, .xml, .csv, .log, .css, .js, .html, .cs, .cshtml, .config, .yml, .yaml
```
Recommended restrictions:
- Only edit files under a maximum size, for example 2 MB.
- Detect encoding as UTF-8 first.
- Save as UTF-8.
- Show a warning for binary-looking content.
- Use optimistic concurrency: include last modified timestamp and reject save if the file changed after opening.
Editor save endpoint:
```text
POST /api/fs/save-text
```
Payload:
```json
{
"path": "docs/readme.md",
"content": "# Updated content",
"knownModifiedAtUtc": "2026-06-11T10:15:00Z"
}
```
## 20. UI behavior checklist
The file browser should include:
- breadcrumb navigation
- back/up controls
- grid and list view
- large folder cards
- rich thumbnails
- file type icons
- upload button
- drag-and-drop upload
- create folder button
- delete confirmation
- download action
- file details/inspector panel
- empty folder state
- loading skeletons
- upload progress
- error toast/dialog
- mobile responsive layout
- keyboard navigation
- focus states
- accessible labels
- localStorage-backed preferences
Minimum keyboard behavior:
| Key | Behavior |
|---|---|
| `Enter` | open selected file/folder |
| `Backspace` | go to parent folder, unless editing text |
| `Delete` | delete selected item after confirm |
| Arrow keys | move selection in grid/list |
| `Escape` | close dialog/viewer/menu |
| `/` | focus search/filter input |
## 21. Playwright test plan
Create a separate test project:
```bash
mkdir tests
cd tests
dotnet new nunit -n Files.PlaywrightTests -f net10.0
cd Files.PlaywrightTests
dotnet add package Microsoft.Playwright.NUnit
dotnet build
pwsh bin/Debug/net10.0/playwright.ps1 install
```
### 21.1 Required test data
Create a test fixture folder copied into `wwwroot` before each test run:
```text
wwwroot/
TestFiles/
image.png
video.mp4
document.pdf
note.txt
unknown.bin
Nested/
child.txt
```
Clear the fixture and `Deleted` folder after tests.
### 21.2 Required Playwright tests
1. **Loads browser shell**
- Opens `/`.
- Verifies toolbar, breadcrumb, grid/list container, upload button, create folder button.
2. **Lists folders and files**
- Opens `/browse?path=TestFiles`.
- Verifies `image.png`, `video.mp4`, `document.pdf`, `note.txt`, `unknown.bin`, and `Nested` appear.
3. **Creates folder**
- Clicks create folder.
- Creates `New Folder`.
- Verifies it appears in the grid.
4. **Uploads file**
- Uploads a fixture file.
- Verifies upload progress and final file card.
5. **Image viewer works**
- Clicks image file.
- Verifies `<img>` viewer is visible and has loaded.
- Verifies download action exists.
6. **Video viewer works**
- Clicks video file.
- Verifies `<video controls>` exists.
- Verifies video element has a source and metadata loads.
7. **PDF viewer works**
- Clicks PDF.
- Verifies PDF viewer frame/object is visible.
- Verifies fallback download action exists.
8. **Text editor works**
- Opens text file.
- Changes content.
- Saves.
- Reloads.
- Verifies new content persisted.
9. **Unsupported file falls back correctly**
- Opens `.bin` file.
- Verifies no inline viewer is attempted.
- Verifies details/download/delete actions.
10. **Delete moves file to Deleted**
- Deletes a file.
- Verifies it disappears from browser.
- Verifies a file exists under `Deleted` on disk.
11. **Protected folders cannot be deleted**
- Attempts to delete `/css` or another protected path.
- Verifies action fails.
12. **Path traversal is rejected**
- Calls API with `../appsettings.json`.
- Verifies 400/403 response.
13. **localStorage settings persist**
- Changes view mode/density.
- Reloads.
- Verifies preference remains.
14. **Visual matching**
- Takes screenshots of core states.
- Compares against approved snapshots.
Example Playwright test skeleton:
```csharp
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace Files.PlaywrightTests;
public class BrowserSmokeTests : PageTest
{
[Test]
public async Task BrowserShell_Loads()
{
await Page.GotoAsync("https://localhost:5001/");
await Expect(Page.GetByTestId("files-toolbar")).ToBeVisibleAsync();
await Expect(Page.GetByTestId("files-breadcrumbs")).ToBeVisibleAsync();
await Expect(Page.GetByTestId("files-grid")).ToBeVisibleAsync();
await Expect(Page.GetByRole(AriaRole.Button, new() { Name = "Upload" })).ToBeVisibleAsync();
await Expect(Page.GetByRole(AriaRole.Button, new() { Name = "New folder" })).ToBeVisibleAsync();
}
}
```
Use stable `data-testid` attributes on important elements.
## 22. Visual regression testing
Because matching the `Designs` folder is a core requirement, add screenshot tests.
Recommended screenshot states:
```text
screenshots/
browser-empty.png
browser-grid-with-folders.png
browser-grid-rich-thumbnails.png
browser-list.png
viewer-image.png
viewer-video.png
viewer-pdf.png
editor-text.png
upload-dialog.png
delete-confirm.png
mobile-browser.png
```
Use fixed test viewport sizes:
- Desktop: `1440x1000`
- Laptop: `1280x800`
- Tablet: `834x1112`
- Mobile: `390x844`
Before snapshotting:
- Disable random animations.
- Use deterministic test files.
- Use fixed modified dates in test setup if displayed.
- Avoid relying on OS-specific video controls for exact pixel matches; test the themed container and action chrome separately.
## 23. Accessibility baseline
Implement from the start:
- visible focus states
- semantic buttons and links
- `aria-label` for icon-only actions
- `aria-live` region for upload progress and errors
- keyboard-operable menus and dialogs
- dialog focus trap
- Escape to close dialogs
- sufficient contrast against the design theme
- no action hidden behind hover only
## 24. Suggested implementation phases
### Phase 1 — Design audit and skeleton
- Create MVC project targeting `net10.0`.
- Add layout, theme CSS files, and base components.
- Inspect `Designs` folder and create `design-audit.md`.
- Implement token-based theme from the design images.
- Build static mock browser screen first.
### Phase 2 — Storage and listing
- Add `FileStorageOptions`.
- Add `PathSafety`.
- Add `FileStorageService`.
- Add folder listing endpoint.
- Render real `wwwroot` contents.
### Phase 3 — File actions
- Add upload.
- Add folder creation.
- Add download.
- Add soft delete to `Deleted`.
- Add antiforgery handling for all mutating endpoints.
### Phase 4 — Module system
- Add module registry.
- Add Default, Images, Video, Audio, PDF, and Text modules.
- Add viewer and thumbnail view components.
- Add unsupported file fallback.
### Phase 5 — Rich UX
- Add drag-and-drop upload.
- Add inspector panel.
- Add localStorage preferences.
- Add keyboard navigation.
- Add responsive layout.
### Phase 6 — Testing
- Add Playwright project.
- Add fixture setup/teardown.
- Add file operation tests.
- Add viewer tests.
- Add visual regression screenshots.
- Add path safety/security tests.
## 25. Definition of done
The implementation is complete when:
- `dotnet build` passes.
- `dotnet test` passes.
- Playwright browsers install and tests run locally.
- `/` displays the themed file browser.
- The UI closely matches `Designs` folder screenshots.
- `wwwroot` contents are listed.
- Protected application folders cannot be deleted.
- Anonymous upload works.
- Anonymous folder creation works.
- Anonymous delete moves files/folders to `Deleted` outside `wwwroot`.
- Download works for every file type.
- Image, video, audio, PDF, and text files open in rich in-browser viewers.
- Text editing and save works for supported text files.
- Unsupported files use the fallback details/download/delete screen.
- Settings persist in localStorage.
- The file module system allows adding a new viewer or thumbnail renderer without modifying the main browser controller/view.
## 26. Notes for future expansion
Good later additions:
- Search/filter inside current folder.
- Recursive search.
- Rename/move/copy.
- Multi-select batch delete/download.
- Zip folder download.
- Monaco editor for code.
- Markdown preview module.
- Archive preview module.
- Server-generated video/PDF thumbnails.
- WebDAV-like API.
- Optional restore browser for `Deleted`.
- Optional auth layer if the app becomes internet-facing.