{"_id":"@bjoernboss/mws-files","name":"@bjoernboss/mws-files","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@bjoernboss/mws-files","version":"1.0.0","type":"module","description":"Module in TypeScript to share and manage files and directories - For Modular Web Server","license":"BSD-3-Clause","author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/BjoernBoss/mws-files.git"},"publishConfig":{"access":"public"},"main":"dist/files.js","types":"dist/files.d.ts","exports":{".":{"types":"./dist/files.d.ts","default":"./dist/files.js"},"./package.json":"./package.json"},"scripts":{"prepare":"tsc"},"dependencies":{"@bjoernboss/mws":"^1.4.0"},"devDependencies":{"@types/node":"^25.6.0","typescript":"^6.0.3"},"engines":{"node":">=22.0.0"},"gitHead":"0b7fcc3eb968a63eb01fc75460d212dde5857d73","_id":"@bjoernboss/mws-files@1.0.0","bugs":{"url":"https://github.com/BjoernBoss/mws-files/issues"},"homepage":"https://github.com/BjoernBoss/mws-files#readme","_nodeVersion":"26.5.1","_npmVersion":"11.13.0","dist":{"integrity":"sha512-K0u1B9iean6EwUUr0o32+GHaD5PJYWO2tWoN8FBB4x45ntZdgZnvRfIr3lTAhTYU3WhAlF9g5f30tITNy7s6oA==","shasum":"3bca2390d98f8e1e53b78c3efcfae735ec468b38","tarball":"https://registry.npmjs.org/@bjoernboss/mws-files/-/mws-files-1.0.0.tgz","fileCount":23,"unpackedSize":200873,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEZyUBggOgFiX6AhV56nM7c9etT8FtPP2Xk4s1egfFPMAiEAoxov05Iu/UdCnq9PThJxeGgvO+AcKH33EPHXxGmbc/c="}]},"_npmUser":{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"},"directories":{},"maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mws-files_1.0.0_1785932196573_0.6102940532947612"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T12:16:36.316Z","1.0.0":"2026-08-05T12:16:36.715Z","modified":"2026-08-05T12:16:36.951Z"},"maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"description":"Module in TypeScript to share and manage files and directories - For Modular Web Server","homepage":"https://github.com/BjoernBoss/mws-files#readme","repository":{"type":"git","url":"git+https://github.com/BjoernBoss/mws-files.git"},"author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"bugs":{"url":"https://github.com/BjoernBoss/mws-files/issues"},"license":"BSD-3-Clause","readme":"# \\[MWS\\] Module to Share Files and Directories\r\n![TypeScript](https://img.shields.io/badge/language-TypeScript-blue?style=flat-square)\r\n[![License](https://img.shields.io/badge/license-BSD--3--Clause-brightgreen?style=flat-square)](LICENSE.txt)\r\n\r\nA file sharing module for [`@bjoernboss/mws`](https://github.com/BjoernBoss/mws).\r\n\r\nIt serves the content of a data directory over HTTP: browsing directories through a full-featured web frontend, downloading files and entire directories as ZIP archives, and - if permitted - uploading, copying, moving, and deleting content. A WebSocket endpoint allows API clients to listen for directory changes.\r\n\r\nAll content is stored as plain files and directories in the configured data directory and persists across server restarts. Path reservations and copy jobs are managed by the `FileShare` module. Note: The module is not case-insensitivity aware. Meaning on file-systems, which do not consider case sensitivity, the uploading might overwrite files wrongfully.\r\n\r\n## Installation\r\n\r\n\t$ npm install @bjoernboss/mws-files\r\n\r\nRequires Node.js 22 or later.\r\n\r\n## Setup\r\n\r\nThe `FileShare` module takes a data directory path and an optional `Params` object controlling what operations clients may perform. Mount it under a path using `dispatch`:\r\n\r\n```typescript\r\nimport { Server, dispatch, addLogger, createConsoleLogger } from \"@bjoernboss/mws\";\r\nimport { FileShare } from \"@bjoernboss/mws-files\";\r\n\r\naddLogger(createConsoleLogger());\r\n\r\nconst server = new Server();\r\nconst share = new FileShare('./data/share', {\r\n    access: true,\r\n    upload: true,\r\n    delete: true\r\n});\r\n\r\nserver.listen(dispatch({ '/share': share }), { port: 8080 });\r\n```\r\n\r\nThe module serves its own pages, static assets, and WebSocket endpoints from its mount point. Navigate to `http://localhost:8080/share/files/` to open the root directory view.\r\n\r\nImportant: The module caches path reservations and copy jobs in memory. The same data directory should therefore not be used by multiple `FileShare` modules simultaneously.\r\n\r\n## Frontend\r\n\r\nThe directory view is an elaborate single-page browser frontend, built entirely on the public API described below. Operations not permitted by the configured parameters are hidden from the UI.\r\n\r\n### Browsing\r\n\r\n- Breadcrumb navigation with home and parent buttons; on narrow screens the breadcrumb scrolls end-favoring, keeping the closest parents visible.\r\n- Directory listing sorted with directories first, showing entry counts, human-readable file sizes, and localized modification dates.\r\n- Per-entry menu (via the menu button or right-click): Open, Download (files directly, directories as ZIP), Copy URL to the clipboard, Rename, Copy to..., Move to..., and Delete.\r\n- Browsing nested directories is performed in-place in the same page, to prevent repeated reloading.\r\n\r\n### Uploading\r\n\r\n- Files and entire directory trees can be uploaded via drag-and-drop anywhere on the page (an animated drop zone appears), or through the create menu (create directory, upload files, upload directory).\r\n- Directory uploads recreate the full tree, creating parent directories before their content.\r\n- Each file upload first reserves the target path, allowing name conflicts to be detected before any data is transferred; the original modification time of uploaded files is preserved (when permitted by the module).\r\n- Files exceeding the configured upload limit are skipped client-side with a notification; the limit is displayed in the create menu and drop zone.\r\n\r\n### Copying, Moving, and Deleting\r\n\r\n- New names are edited inline in the listing itself (Enter confirms, Escape aborts), with client-side name validation.\r\n- The copy and move targets are chosen through a directory picker dialog, which allows navigating the whole share and creating new directories on the way; an in-place copy proposes a free `- Copy (n)` name.\r\n- Directory copies and deletions are performed recursively by the frontend: the tree is enumerated first, then processed file-by-file (copied files preserve their modification times; directory modification times only when permitted by the module).\r\n- Deletions must be confirmed through a dialog showing the full path.\r\n\r\n### Progress and Robustness\r\n\r\n- All operations report through stacked toast notifications with status texts, per-file progress bars, and overall counters; copy jobs are polled once per second and their progress is animated using a speculative forecast between polls.\r\n- Bulk operations run at most 3 remote requests concurrently, skip entries whose parent operation failed, and abort after 12 failures.\r\n- A live change-listener WebSocket connection is established to update the view on changes.\r\n- The listing is updated optimistically after each successful operation, without re-fetching the directory, as this will be reported through the change-listener.\r\n- Navigating away is guarded by a confirmation prompt while operations are still running.\r\n\r\n### State Caching\r\n\r\nFor any client, all requests reaching the same rebase should have the same `Params` and `Endpoints` mappings. The frontend caches the endpoints and parameters, and will re-use these assumptions for any sub-path within its `rebase` confinement. Dynamic `Params` or `Endpoints` for one client, depending on the accessed directory, may result in unexpected or unintended assumptions and behavior of the frontend.\r\n\r\n## Parameters\r\n\r\nThe `Params` object controls module behavior and access. All fields are optional:\r\n\r\n| Field | Default | Description |\r\n|---|---|---|\r\n| `access` | `false` | Access content at all - browsing, downloading, copy jobs, and change listeners all require it |\r\n| `upload` | `false` | Upload files, create directories, and copy or move content |\r\n| `delete` | `false` | Delete content (also required to move content) |\r\n| `uploadMTime` | `false` | Preserve the client-supplied modification time of uploads, otherwise reset to current time |\r\n| `uploadLimit` | `100000000` (100 MB) | Largest content to upload or copy in bytes (`0` implies no limit) |\r\n| `rebase` | `'/'` | Sub-directory of the share to serve as the connection's root (must be a directory) |\r\n\r\nWithout any parameters the share is inaccessible; `access: true` alone yields a read-only share. Parameters can also be set per-request through `params` when dispatching to the module. Request parameters override the corresponding default, allowing parent modules to implement authentication or per-route access policies.\r\n\r\nWith `rebase`, all paths of the connection - served content, copy and move targets, and watched directories - are resolved relative to the given sub-directory, and nothing outside of it can be reached. The rebasing is fully transparent to clients, which makes per-request `rebase` suitable for handing each user their own root within a shared data directory.\r\n\r\n## Endpoints\r\n\r\nThe `Endpoints` export provides the path constants used by the module. All paths are relative to the module's mount point. Path components in the URL use URI encoding while preserving `/`; paths in JSON payloads are not encoded.\r\n\r\n| Path | Method | Description |\r\n|---|---|---|\r\n| `/files/{path}` | GET | Serve a file, or a directory as HTML view, JSON listing, or ZIP download |\r\n| `/files/{path}` | POST | Upload a file, create a directory, or reserve a path (requires `Params.upload`) |\r\n| `/files/{path}` | PUT | Copy or move content to a new path (requires `Params.upload`; move also `Params.delete`) |\r\n| `/files/{path}` | DELETE | Delete a file or an empty directory (requires `Params.delete`) |\r\n| `/jobs/{id}` | GET | JSON status of a copy job |\r\n| `/static/*` | GET | Static assets (CSS, JS, icons) served with immutable cache headers |\r\n| `/ws/{path}` | WebSocket | Listen for changes of a directory |\r\n\r\nAll endpoints except `/static` additionally require `Params.access`; without it they respond with 403.\r\n\r\nPath components may not contain control characters or any of `/ \\ ? : * \" < > |`. Entries in the data directory whose names contain such characters (created externally) are excluded from all listings, downloads, and change notifications. The (possibly rebased) root directory itself can only be read, never modified, and cannot be the direct target of a copy or move, but can be copied/moved into.\r\n\r\n## Files API\r\n\r\nEvery response of the `/files` endpoint carries a `Kind` header (`file` or `directory`) identifying what was served, and a `Path` header echoing the served path in the shared file space (URI-encoded like paths in the URL). The optional query parameter `kind=file|directory` restricts the request to the given kind; a mismatch results in 409. For requests without `kind`, a file is preferred over a directory of the same path.\r\n\r\n### Reading (GET)\r\n\r\n- A file is served directly (range requests and content encoding are handled by the framework); `download=true` adds a `Content-Disposition: attachment` header (the filename is given as `filename*`, with an alternative ascii `filename` fallback).\r\n- A directory is served as the interactive HTML browser view by default. With `download=true` the directory is streamed as a ZIP archive (`{name}.zip`), and with `raw=true` the JSON listing is returned instead.\r\n\r\nThe JSON listing maps entry names to their metadata:\r\n\r\n```json\r\n{ \"example.txt\": { \"kind\": \"file\", \"size\": 1234, \"modified\": 1710000000000 } }\r\n```\r\n\r\nFor directories, `size` is the number of contained entries; `modified` is the modification time in milliseconds since the epoch. ZIP downloads are streamed using ZIP64 extensions; compressible media types are deflated, all other content is stored uncompressed.\r\n\r\n### Uploading (POST)\r\n\r\n- `kind=file` (default): the request body becomes the file content; fails with 409 if the path already exists. The body size is limited by `Params.uploadLimit` (413 if exceeded).\r\n- `kind=directory`: creates an empty directory; with `silent=true`, an already existing directory responds OK instead of 409 (this also applies to `reserve=true` requests, which then respond without a `Reservation-Id` header).\r\n- `mtime={ms}`: sets the modification time of the created content (only honored when `Params.uploadMTime` is enabled).\r\n- `reserve=true`: instead of uploading, reserves the path for 5 seconds and responds with a `Reservation-Id` header. While a reservation is active, only requests passing the id back via `reservation={id}` may claim the path. This allows clients to atomically pick a free name before starting a large upload.\r\n\r\nIn all cases the parent directory of the path must already exist; intermediate directories are never created implicitly.\r\n\r\n### Copying and Moving (PUT)\r\n\r\nExactly one of `copy={target}` or `move={target}` must be given, where the target is the full destination path within the share (given decoded in the query string). The destination must not exist yet and its parent directory must exist; `reservation={id}` may pass a previously created reservation for the destination.\r\n\r\n- `move`: renames the file or directory (`kind` selects the expected source kind). Requires `Params.upload` and `Params.delete`.\r\n- `copy`: only files can be copied, and `Params.uploadLimit` is enforced on the source size (413 if exceeded). The copy runs as a background job: the response carries a `Job-Id` header, and progress can be polled via `/jobs/{id}`. Requires `Params.upload`.\r\n\r\n### Deleting (DELETE)\r\n\r\nDeletes the file or empty directory at the path (`kind` selects the expected kind). Deleting a non-empty directory responds with 409.\r\n\r\n## Copy Jobs\r\n\r\nA copy job created by PUT `copy` can be polled via GET `/jobs/{id}`:\r\n\r\n```json\r\n{ \"progress\": 0.5, \"state\": \"running\", \"message\": \"\" }\r\n```\r\n\r\n`state` is one of `running`, `success`, or `failure`; `message` describes failures and `progress` ranges from 0 to 1. Finished job states are retained for 3 minutes before being cleaned up. Running jobs are aborted when the server shuts down, in which case the partially written target is removed.\r\n\r\n## WebSocket Protocol\r\n\r\nClients connect to `/ws/{path}` to listen for changes of a directory (only directories can be watched). Whenever the directory content changes, the server broadcasts the full JSON directory listing (same format as `raw=true`). Notifications are coalesced over a timespan of a few seconds. Changes within immediate sub-directories, which alter the listed entry count and modification time, are tracked as well. Clients never need to send data, as this is only a notification channel.\r\n\r\nBesides listings, the server sends one of three string identifiers:\r\n\r\n- **`\"removed\"`**: the watched directory was deleted.\r\n- **`\"error\"`**: watching the directory failed.\r\n- **`\"close\"`**: the server is shutting down.\r\n\r\nAfter an identifier is sent, the server closes the WebSocket. The underlying file-system watcher is kept alive for a certain grace period after the last listener disconnects, to handle quick reconnections.\r\n","readmeFilename":"README.md","_rev":"1-ef379f6503a8ce1471487a7b7d7f0592"}