@nicemeta/file-manager 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ @font-face{font-family:'Material Symbols';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/symbols.woff2') format('woff2')}:root{--bg:#0d1117;--bg-card:#151c2c;--bg-card-hover:#1c2438;--bg-surface:#151c2c;--bg-hover:#1c2438;--bg-active:#243044;--text:#e0e6ed;--text-muted:#546178;--accent:#6c7ee1;--accent-hover:#8b9bf0;--icon-color:#6c7ee1;--delete-color:#df4d4d;--drop-ok:#4caf50;--drop-reject:#f44336;--radius:10px;--radius-sm:6px;--transition:0.18s ease;--zoom:1}*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:14px;line-height:1.5;height:100%;overflow:hidden}body{display:flex;flex-direction:column;padding:16px 24px;gap:8px}a{color:var(--accent);text-decoration:none;transition:color var(--transition)}a:hover{color:var(--accent-hover)}.breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:2px;padding:6px 8px;font-size:13px;min-height:32px;flex-shrink:0}.breadcrumb a{padding:2px 6px;border-radius:var(--radius-sm);transition:background var(--transition),color var(--transition);color:var(--text)}.breadcrumb a:hover{background:var(--bg-hover);color:var(--accent-hover)}#content-pane{flex:1;overflow:hidden;border-radius:var(--radius);border:2px solid transparent;transition:border-color 0.25s ease,box-shadow 0.25s ease;position:relative;display:flex;flex-direction:column}#content-pane.drop-zone-active{border-color:var(--drop-ok);box-shadow:inset 0 0 40px rgba(76,175,80,0.10)}#content-pane.drop-zone-reject{border-color:var(--drop-reject);box-shadow:inset 0 0 40px rgba(244,67,54,0.10)}.file-list{list-style:none;flex:1;overflow-y:auto;overflow-x:hidden;padding:4px 0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));grid-auto-rows:min-content;gap:12px;align-content:start;transform:scale(var(--zoom));transform-origin:top left;width:calc(100% / var(--zoom));height:calc(100% / var(--zoom))}.file-list::-webkit-scrollbar{width:6px}.file-list::-webkit-scrollbar-track{background:transparent}.file-list::-webkit-scrollbar-thumb{background:#2a3548;border-radius:3px}.file-list::-webkit-scrollbar-thumb:hover{background:#3a4a60}.file-list .section-header{grid-column:1 / -1;font-size:13px;font-weight:600;color:var(--text);padding:8px 4px 0;border:none;background:none;min-height:auto;cursor:default}.file-list .section-header:hover{background:none}.file-list li{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:var(--radius);background:var(--bg-card);cursor:default;transition:background var(--transition);min-height:72px;overflow:hidden;position:relative}.file-list li:hover{background:var(--bg-card-hover)}.file-list li.folder-drop-hover{background:rgba(76,175,80,0.12);outline:2px solid var(--drop-ok);outline-offset:-2px}.mi{font-family:'Material Symbols';font-style:normal;font-weight:normal;text-rendering:optimizeLegibility;font-feature-settings:'liga';font-variation-settings:'FILL' 0,'GRAD' 0,'opsz' 48,'wght' 400;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.mi-filled{font-variation-settings:'FILL' 1,'GRAD' 0,'opsz' 48,'wght' 400}.card-icon{flex-shrink:0;width:44px;height:44px;font-size:44px;line-height:1;color:var(--icon-color)}.card-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}.card-info .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-size:13px;font-weight:500}.card-info .meta{font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.directory .card-link{display:flex;align-items:center;gap:14px;color:var(--text);flex:1;min-width:0}.directory .card-link:hover{color:var(--text)}.directory .card-link:hover .name{color:var(--accent-hover)}.file .card-content{display:flex;align-items:center;gap:14px;flex:1;min-width:0}.item-tool{display:none;align-items:center;gap:4px;flex-shrink:0;position:absolute;top:6px;right:6px}li:hover .item-tool{display:inline-flex}.item-tool a{font-size:18px;opacity:0.5;transition:opacity var(--transition),color var(--transition);padding:3px;line-height:1;border-radius:4px;color:var(--text-muted)}.item-tool a:hover{opacity:1;background:var(--bg-active)}.item-tool a.delete{color:var(--delete-color);opacity:0.6}.item-tool a.delete:hover{color:#ff6b6b;opacity:1}.item-tool a.download:hover{color:var(--accent-hover)}.item-tool a.view:hover{color:var(--text)}#zoom-control{display:flex;align-items:center;gap:8px;padding:6px 12px;flex-shrink:0;font-size:11px;color:var(--text-muted);user-select:none}#zoom-control label{white-space:nowrap}#zoom-control input[type="range"]{-webkit-appearance:none;appearance:none;width:100px;height:3px;background:#1c2438;border-radius:2px;outline:none;cursor:pointer}#zoom-control input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;background:var(--accent);border-radius:50%;cursor:pointer;transition:background var(--transition)}#zoom-control input[type="range"]::-webkit-slider-thumb:hover{background:var(--accent-hover)}#zoom-control input[type="range"]::-moz-range-thumb{width:12px;height:12px;background:var(--accent);border-radius:50%;border:none;cursor:pointer}#zoom-control span{min-width:32px;text-align:right}#mobile-upload{display:none}@media (pointer:coarse){#mobile-upload{display:inline-flex;align-items:center;justify-content:center;position:fixed;bottom:60px;right:20px;width:48px;height:48px;border-radius:50%;background:var(--accent);color:#fff;font-size:24px;border:none;cursor:pointer;box-shadow:0 2px 12px rgba(0,0,0,0.5);z-index:100}#mobile-upload .mi{font-size:24px}#mobile-upload:hover{background:var(--accent-hover)}}#drop-message{display:none;position:absolute;inset:0;background:rgba(13,17,23,0.88);color:var(--text);font-size:18px;align-items:center;justify-content:center;border-radius:var(--radius);pointer-events:none;z-index:10}#content-pane.drop-zone-active #drop-message,#content-pane.drop-zone-reject #drop-message{display:flex}#confirm-overlay{display:none;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,0.6);align-items:center;justify-content:center}#confirm-overlay.visible{display:flex}#confirm-panel{background:var(--bg-card);border:1px solid #1c2438;border-radius:var(--radius);padding:24px 28px;min-width:320px;max-width:440px;box-shadow:0 12px 40px rgba(0,0,0,0.6);display:flex;flex-direction:column;gap:16px}#confirm-panel .confirm-title{font-size:15px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px}#confirm-panel .confirm-title .mi{color:var(--delete-color);font-size:22px}#confirm-panel .confirm-message{font-size:13px;color:var(--text-muted);word-break:break-all}#confirm-panel .confirm-buttons{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}#confirm-panel button{padding:6px 20px;border-radius:var(--radius-sm);border:1px solid #1c2438;font-size:13px;cursor:pointer;transition:background var(--transition),border-color var(--transition)}#confirm-panel .btn-cancel{background:var(--bg-hover);color:var(--text)}#confirm-panel .btn-cancel:hover{background:var(--bg-active)}#confirm-panel .btn-ok{background:var(--delete-color);color:#fff;border-color:var(--delete-color)}#confirm-panel .btn-ok:hover{background:#e85858;border-color:#e85858}
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>File Manager</title>
8
+ <link rel="stylesheet" href="css/file-manager.css">
9
+ </head>
10
+
11
+ <body>
12
+
13
+ <p id="breadCrumb" class="breadcrumb"></p>
14
+
15
+ <div id="content-pane">
16
+ <div id="drop-message">Drop files to upload</div>
17
+ <ul id="file-list" class="file-list"></ul>
18
+ </div>
19
+
20
+ <!-- Mobile-only upload button (touch devices without drag-and-drop) -->
21
+ <button id="mobile-upload" title="Upload files"><i class="mi">upload</i></button>
22
+ <input type="file" id="mobile-file-input" multiple style="display:none">
23
+
24
+ <div id="zoom-control">
25
+ <label for="zoom-slider">Zoom</label>
26
+ <input type="range" id="zoom-slider" min="0.7" max="2" step="0.05" value="1">
27
+ <span id="zoom-value">100%</span>
28
+ </div>
29
+
30
+ <!-- Delete confirmation overlay -->
31
+ <div id="confirm-overlay">
32
+ <div id="confirm-panel">
33
+ <div class="confirm-title"><i class="mi">delete</i> Confirm Delete</div>
34
+ <div class="confirm-message" id="confirm-message"></div>
35
+ <div class="confirm-buttons">
36
+ <button class="btn-cancel" id="confirm-cancel">Cancel</button>
37
+ <button class="btn-ok" id="confirm-ok">Delete</button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <script>"use strict";const API={list:"api/list?path=",download:"api/download?path=",zip:"api/zip?path=",view:"api/view?path=",delete:"api/delete?path=",upload:"api/upload?path="},fmUri="?path=",ZOOM_BASE=1.05,contentPane=document.getElementById("content-pane"),fileList=document.getElementById("file-list"),breadCrumb=document.getElementById("breadCrumb"),dropMessage=document.getElementById("drop-message"),zoomSlider=document.getElementById("zoom-slider"),zoomValue=document.getElementById("zoom-value"),mobileUpload=document.getElementById("mobile-upload"),mobileFileInput=document.getElementById("mobile-file-input"),confirmOverlay=document.getElementById("confirm-overlay"),confirmMessage=document.getElementById("confirm-message"),confirmOk=document.getElementById("confirm-ok"),confirmCancel=document.getElementById("confirm-cancel");let currentPath="",currentWritable=!1,dragCounter=0,scrollDebounce=null,pendingDeleteUrl=null;function encodePath(e){return encodeURIComponent(e).replace(/%2F/g,"/")}function formatSize(e){if(e==null)return"\u2014";if(e===0)return"0 B";const n=["B","KB","MB","GB","TB"],o=Math.floor(Math.log(e)/Math.log(1024)),r=e/Math.pow(1024,o);return(o===0?r:r.toFixed(2))+" "+n[o]}function timeAgo(e){if(!e)return"";const n=new Date(e);if(isNaN(n.getTime()))return"";const o=Math.floor((Date.now()-n.getTime())/1e3);if(o<60)return"just now";const r=[[31536e3,"year"],[2592e3,"month"],[86400,"day"],[3600,"hour"],[60,"minute"]];for(const[a,s]of r){const i=Math.floor(o/a);if(i>=1)return i===1?"a "+s+" ago":i+" "+s+"s ago"}return"just now"}function applyZoom(e){const n=e*ZOOM_BASE;document.documentElement.style.setProperty("--zoom",n),zoomSlider.value=e,zoomValue.textContent=Math.round(e*100)+"%",localStorage.setItem("fm-zoom",e)}(function(){const n=localStorage.getItem("fm-zoom"),o=parseFloat(zoomSlider.min),r=parseFloat(zoomSlider.max),a=n?Math.min(r,Math.max(o,parseFloat(n))):1;applyZoom(a)})(),zoomSlider.addEventListener("input",()=>applyZoom(parseFloat(zoomSlider.value)));function saveScroll(){currentPath!==void 0&&localStorage.setItem("fm-scroll-"+currentPath,fileList.scrollTop)}function restoreScroll(){const e=localStorage.getItem("fm-scroll-"+currentPath);e&&(fileList.scrollTop=parseInt(e,10))}fileList.addEventListener("scroll",()=>{clearTimeout(scrollDebounce),scrollDebounce=setTimeout(saveScroll,200)});function uploadFiles(e,n){if(!e||e.length===0)return;const o=new FormData;for(const a of e)o.append("file",a);const r=API.upload+encodePath(n);fetch(r,{method:"POST",body:o}).then(()=>loadDirectory(currentPath)).catch(a=>console.error("Upload failed:",a))}document.addEventListener("dragover",e=>e.preventDefault()),document.addEventListener("drop",e=>e.preventDefault()),contentPane.addEventListener("dragenter",e=>{e.preventDefault(),dragCounter++,dragCounter===1&&(contentPane.classList.add(currentWritable?"drop-zone-active":"drop-zone-reject"),dropMessage.textContent=currentWritable?"Drop files to upload":"This folder is read-only")}),contentPane.addEventListener("dragover",e=>{e.preventDefault(),e.dataTransfer.dropEffect=currentWritable?"copy":"none"}),contentPane.addEventListener("dragleave",e=>{e.preventDefault(),dragCounter--,dragCounter<=0&&(dragCounter=0,contentPane.classList.remove("drop-zone-active","drop-zone-reject"))}),contentPane.addEventListener("drop",e=>{e.preventDefault(),dragCounter=0,contentPane.classList.remove("drop-zone-active","drop-zone-reject"),currentWritable&&e.dataTransfer.files.length>0&&uploadFiles(e.dataTransfer.files,currentPath)}),mobileUpload.addEventListener("click",()=>mobileFileInput.click()),mobileFileInput.addEventListener("change",()=>{mobileFileInput.files.length>0&&currentWritable&&(uploadFiles(mobileFileInput.files,currentPath),mobileFileInput.value="")});function requestDelete(e,n){pendingDeleteUrl=e,confirmMessage.textContent="Are you sure you want to delete \u201C"+n+"\u201D?",confirmOverlay.classList.add("visible")}confirmOk.addEventListener("click",()=>{confirmOverlay.classList.remove("visible"),pendingDeleteUrl&&fetch(pendingDeleteUrl,{method:"DELETE"}).then(()=>{pendingDeleteUrl=null,loadDirectory(currentPath)}).catch(e=>{pendingDeleteUrl=null,console.error("Delete failed:",e)})}),confirmCancel.addEventListener("click",()=>{confirmOverlay.classList.remove("visible"),pendingDeleteUrl=null}),confirmOverlay.addEventListener("click",e=>{e.target===confirmOverlay&&(confirmOverlay.classList.remove("visible"),pendingDeleteUrl=null)}),document.addEventListener("keydown",e=>{confirmOverlay.classList.contains("visible")&&(e.key==="Escape"?confirmCancel.click():e.key==="Enter"&&confirmOk.click())});function createItemTools(e,n,o,r,a){const s=document.createElement("span");s.classList.add("item-tool");const i=document.createElement("a");if(i.classList.add("download","mi"),i.href=o?API.zip+e:API.download+e,i.title="Download",i.textContent="download",s.appendChild(i),!o){const t=document.createElement("a");t.classList.add("view","mi"),t.href=API.view+e,t.target="_blank",t.title="View",t.textContent="open_in_new",s.appendChild(t)}if(!a&&r){const t=document.createElement("a");t.classList.add("delete","mi"),t.href="javascript:void(0)",t.title="Delete",t.textContent="delete",t.addEventListener("click",d=>{d.preventDefault(),requestDelete(API.delete+e,n)}),s.appendChild(t)}return s}function createItem(e){const n=e.parent?encodePath(e.parent+(e.parent.endsWith("/")?"":"/")+e.name):e.name,o=document.createElement("li");o.classList.add(e.isDirectory?"directory":"file");const r=document.createElement("i");r.classList.add("mi","card-icon"),e.isDirectory?(r.classList.add("mi-filled"),r.textContent="folder"):r.textContent="description";const a=document.createElement("div");a.classList.add("card-info");const s=document.createElement("span");s.classList.add("name"),s.textContent=e.isDirectory?e.name.replace(/:[/]$/,""):e.name,a.appendChild(s);const i=document.createElement("span");if(i.classList.add("meta"),i.textContent=e.isDirectory?"\u2014":formatSize(e.length),a.appendChild(i),e.modified){const t=document.createElement("span");t.classList.add("meta"),t.textContent=timeAgo(e.modified),a.appendChild(t)}if(e.isDirectory){const t=document.createElement("a");t.classList.add("card-link"),t.href="javascript:void(0)",t.addEventListener("click",()=>navigateTo(n)),t.appendChild(r),t.appendChild(a),o.appendChild(t)}else{const t=document.createElement("span");t.classList.add("card-content"),t.appendChild(r),t.appendChild(a),o.appendChild(t)}if(o.appendChild(createItemTools(n,e.name,e.isDirectory,e.isWritable,e.hasChildren)),e.isDirectory){let t=0;o.addEventListener("dragenter",d=>{d.preventDefault(),d.stopPropagation(),t++,t===1&&e.isWritable&&o.classList.add("folder-drop-hover")}),o.addEventListener("dragover",d=>{d.preventDefault(),d.stopPropagation(),d.dataTransfer.dropEffect=e.isWritable?"copy":"none"}),o.addEventListener("dragleave",d=>{d.preventDefault(),d.stopPropagation(),t--,t<=0&&(t=0,o.classList.remove("folder-drop-hover"))}),o.addEventListener("drop",d=>{if(d.preventDefault(),d.stopPropagation(),t=0,o.classList.remove("folder-drop-hover"),dragCounter=0,contentPane.classList.remove("drop-zone-active","drop-zone-reject"),e.isWritable&&d.dataTransfer.files.length>0){const c=e.parent?e.parent+(e.parent.endsWith("/")?"":"/")+e.name:e.name;uploadFiles(d.dataTransfer.files,c)}})}return o}function createSectionHeader(e){const n=document.createElement("li");return n.classList.add("section-header"),n.textContent=e,n}function createBreadCrumb(e,n){const o=document.createElement("span");return e&&(e.slice().reverse().forEach(r=>{const a=document.createElement("a");a.href="javascript:void(0)",a.addEventListener("click",()=>navigateTo(r.path)),a.textContent=r.name,o.appendChild(a),o.appendChild(document.createTextNode(" / "))}),o.appendChild(document.createTextNode(n))),o}function loadDirectory(e){fetch(API.list+encodePath(e)).then(n=>n.json()).then(n=>{currentWritable=n.isWritable,contentPane.dataset.writable=n.isWritable,breadCrumb.innerHTML="",breadCrumb.style.display=n.path==="/"?"none":"",breadCrumb.appendChild(createBreadCrumb(n.parentInfos,n.name)),fileList.innerHTML="";const o=!1;if(n.files&&n.files.length>0){const r=[],a=[];n.files.forEach(s=>{(!s.name.startsWith(".")||o)&&(s.isDirectory?r:a).push(s)}),r.length>0&&(fileList.appendChild(createSectionHeader("Folders")),r.forEach(s=>fileList.appendChild(createItem(s)))),a.length>0&&(fileList.appendChild(createSectionHeader("Files")),a.forEach(s=>fileList.appendChild(createItem(s))))}requestAnimationFrame(restoreScroll)})}function navigateTo(e){saveScroll(),currentPath=e||"",window.history.pushState(currentPath,"",fmUri+encodePath(currentPath)),loadDirectory(currentPath)}window.addEventListener("popstate",e=>{saveScroll(),currentPath=e.state!=null?e.state:"",loadDirectory(currentPath)});const params=new URLSearchParams(window.location.search),initPath=params.has("path")?params.get("path"):"";currentPath=initPath,window.history.replaceState(initPath,"",fmUri+encodePath(initPath)),loadDirectory(initPath);</script>
43
+ </body>
44
+ </html>
@@ -0,0 +1 @@
1
+ {"openapi":"3.0.3","info":{"title":"File Manager API","description":"File system browsing, upload, download, and management","version":"0.1.0"},"servers":[{"url":"/"}],"tags":[{"name":"File Manager","description":"File system browsing, upload, download, and management"}],"paths":{"/file-manager/api/list":{"get":{"tags":["File Manager"],"summary":"List directory contents","description":"Returns the contents of the specified directory including file metadata","parameters":[{"name":"path","in":"query","description":"Absolute path of the directory to list","schema":{"type":"string","default":"/"}}],"responses":{"200":{"description":"Directory listing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryInfo"}}}}}}},"/file-manager/api/download":{"get":{"tags":["File Manager"],"summary":"Download a file","description":"Downloads the specified file as an attachment","parameters":[{"name":"path","in":"query","required":true,"description":"Absolute path of the file to download","schema":{"type":"string"}}],"responses":{"200":{"description":"File content","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Path is not a file"}}}},"/file-manager/api/zip":{"get":{"tags":["File Manager"],"summary":"Download as ZIP","description":"Downloads the specified file or directory as a ZIP archive","parameters":[{"name":"path","in":"query","required":true,"description":"Absolute path of the file or directory to zip","schema":{"type":"string"}}],"responses":{"200":{"description":"ZIP archive","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}}}}},"/file-manager/api/view":{"get":{"tags":["File Manager"],"summary":"View a file inline","description":"Returns the file content for inline viewing in the browser","parameters":[{"name":"path","in":"query","required":true,"description":"Absolute path of the file to view","schema":{"type":"string"}}],"responses":{"200":{"description":"File content (inline)","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Path is not a file"}}}},"/file-manager/api/delete":{"delete":{"tags":["File Manager"],"summary":"Delete a file or directory","description":"Recursively deletes the specified file or directory","parameters":[{"name":"path","in":"query","required":true,"description":"Absolute path of the file or directory to delete","schema":{"type":"string"}}],"responses":{"204":{"description":"Successfully deleted"}}}},"/file-manager/api/upload":{"post":{"tags":["File Manager"],"summary":"Upload files to a directory","description":"Uploads one or more files to the specified directory","parameters":[{"name":"path","in":"query","required":true,"description":"Absolute path of the target directory","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"200":{"description":"Files uploaded successfully"}}}},"/file-manager/api/mode":{"put":{"tags":["File Manager"],"summary":"Change file permissions","description":"Changes read/write/execute permissions. Mode format: +rwx or -rwx","parameters":[{"name":"path","in":"query","required":true,"description":"Absolute path of the file","schema":{"type":"string"}},{"name":"mode","in":"query","required":true,"description":"Permission mode (e.g. +rw, -x)","schema":{"type":"string"}}],"responses":{"200":{"description":"Permissions changed"}}}}},"components":{"schemas":{"DirectoryInfo":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"},"isWritable":{"type":"boolean"},"parentInfos":{"type":"array","items":{"$ref":"#/components/schemas/ParentInfo"}},"files":{"type":"array","items":{"$ref":"#/components/schemas/FileInfo"}}}},"ParentInfo":{"type":"object","properties":{"path":{"type":"string"},"name":{"type":"string"}}},"FileInfo":{"type":"object","properties":{"parent":{"type":"string"},"name":{"type":"string"},"length":{"type":"integer","format":"int64"},"isDirectory":{"type":"boolean"},"isReadable":{"type":"boolean"},"isWritable":{"type":"boolean"},"isExecutable":{"type":"boolean"},"hasChildren":{"type":"boolean"},"modified":{"type":"string","description":"ISO local date-time (no timezone)"},"created":{"type":"string","description":"ISO local date-time (no timezone)"}}}}}}
@@ -0,0 +1,219 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: File Manager API
4
+ description: File system browsing, upload, download, and management
5
+ version: 0.1.0
6
+ servers:
7
+ - url: /
8
+ tags:
9
+ - name: File Manager
10
+ description: File system browsing, upload, download, and management
11
+ paths:
12
+ /file-manager/api/list:
13
+ get:
14
+ tags: [File Manager]
15
+ summary: List directory contents
16
+ description: Returns the contents of the specified directory including file metadata
17
+ parameters:
18
+ - name: path
19
+ in: query
20
+ description: Absolute path of the directory to list
21
+ schema:
22
+ type: string
23
+ default: /
24
+ responses:
25
+ '200':
26
+ description: Directory listing
27
+ content:
28
+ application/json:
29
+ schema:
30
+ $ref: '#/components/schemas/DirectoryInfo'
31
+
32
+ /file-manager/api/download:
33
+ get:
34
+ tags: [File Manager]
35
+ summary: Download a file
36
+ description: Downloads the specified file as an attachment
37
+ parameters:
38
+ - name: path
39
+ in: query
40
+ required: true
41
+ description: Absolute path of the file to download
42
+ schema:
43
+ type: string
44
+ responses:
45
+ '200':
46
+ description: File content
47
+ content:
48
+ application/octet-stream:
49
+ schema:
50
+ type: string
51
+ format: binary
52
+ '400':
53
+ description: Path is not a file
54
+
55
+ /file-manager/api/zip:
56
+ get:
57
+ tags: [File Manager]
58
+ summary: Download as ZIP
59
+ description: Downloads the specified file or directory as a ZIP archive
60
+ parameters:
61
+ - name: path
62
+ in: query
63
+ required: true
64
+ description: Absolute path of the file or directory to zip
65
+ schema:
66
+ type: string
67
+ responses:
68
+ '200':
69
+ description: ZIP archive
70
+ content:
71
+ application/zip:
72
+ schema:
73
+ type: string
74
+ format: binary
75
+
76
+ /file-manager/api/view:
77
+ get:
78
+ tags: [File Manager]
79
+ summary: View a file inline
80
+ description: Returns the file content for inline viewing in the browser
81
+ parameters:
82
+ - name: path
83
+ in: query
84
+ required: true
85
+ description: Absolute path of the file to view
86
+ schema:
87
+ type: string
88
+ responses:
89
+ '200':
90
+ description: File content (inline)
91
+ content:
92
+ application/octet-stream:
93
+ schema:
94
+ type: string
95
+ format: binary
96
+ '400':
97
+ description: Path is not a file
98
+
99
+ /file-manager/api/delete:
100
+ delete:
101
+ tags: [File Manager]
102
+ summary: Delete a file or directory
103
+ description: Recursively deletes the specified file or directory
104
+ parameters:
105
+ - name: path
106
+ in: query
107
+ required: true
108
+ description: Absolute path of the file or directory to delete
109
+ schema:
110
+ type: string
111
+ responses:
112
+ '204':
113
+ description: Successfully deleted
114
+
115
+ /file-manager/api/upload:
116
+ post:
117
+ tags: [File Manager]
118
+ summary: Upload files to a directory
119
+ description: Uploads one or more files to the specified directory
120
+ parameters:
121
+ - name: path
122
+ in: query
123
+ required: true
124
+ description: Absolute path of the target directory
125
+ schema:
126
+ type: string
127
+ requestBody:
128
+ required: true
129
+ content:
130
+ multipart/form-data:
131
+ schema:
132
+ type: object
133
+ properties:
134
+ file:
135
+ type: array
136
+ items:
137
+ type: string
138
+ format: binary
139
+ responses:
140
+ '200':
141
+ description: Files uploaded successfully
142
+
143
+ /file-manager/api/mode:
144
+ put:
145
+ tags: [File Manager]
146
+ summary: Change file permissions
147
+ description: "Changes read/write/execute permissions. Mode format: +rwx or -rwx"
148
+ parameters:
149
+ - name: path
150
+ in: query
151
+ required: true
152
+ description: Absolute path of the file
153
+ schema:
154
+ type: string
155
+ - name: mode
156
+ in: query
157
+ required: true
158
+ description: "Permission mode (e.g. +rw, -x)"
159
+ schema:
160
+ type: string
161
+ responses:
162
+ '200':
163
+ description: Permissions changed
164
+
165
+ components:
166
+ schemas:
167
+ DirectoryInfo:
168
+ type: object
169
+ properties:
170
+ name:
171
+ type: string
172
+ path:
173
+ type: string
174
+ isWritable:
175
+ type: boolean
176
+ parentInfos:
177
+ type: array
178
+ items:
179
+ $ref: '#/components/schemas/ParentInfo'
180
+ files:
181
+ type: array
182
+ items:
183
+ $ref: '#/components/schemas/FileInfo'
184
+
185
+ ParentInfo:
186
+ type: object
187
+ properties:
188
+ path:
189
+ type: string
190
+ name:
191
+ type: string
192
+
193
+ FileInfo:
194
+ type: object
195
+ properties:
196
+ parent:
197
+ type: string
198
+ name:
199
+ type: string
200
+ length:
201
+ type: integer
202
+ format: int64
203
+ isDirectory:
204
+ type: boolean
205
+ isReadable:
206
+ type: boolean
207
+ isWritable:
208
+ type: boolean
209
+ isExecutable:
210
+ type: boolean
211
+ hasChildren:
212
+ type: boolean
213
+ modified:
214
+ type: string
215
+ description: ISO local date-time (no timezone)
216
+ created:
217
+ type: string
218
+ description: ISO local date-time (no timezone)
219
+
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>File Manager API</title>
7
+ </head>
8
+ <body>
9
+ <script
10
+ id="api-reference"
11
+ data-url="/file-manager/api-docs/openapi.json"
12
+ data-configuration='{"darkMode":true,"hideDownloadButton":true,"hiddenClients":["ruby","php","python","csharp","clojure","c","objc","ocaml","r","swift","kotlin","powershell"]}'></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
14
+ </body>
15
+ </html>
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@nicemeta/file-manager",
3
+ "version": "0.6.4",
4
+ "description": "File manager web application",
5
+ "main": "src/app.js",
6
+ "bin": {
7
+ "file-manager": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "static/",
13
+ "dist/"
14
+ ],
15
+ "keywords": [
16
+ "file-manager",
17
+ "file-browser",
18
+ "upload",
19
+ "download",
20
+ "cli"
21
+ ],
22
+ "license": "MIT",
23
+ "scripts": {
24
+ "build": "node build.js",
25
+ "clean": "rm -rf dist",
26
+ "start": "node bin/cli.js",
27
+ "start:prod": "node dist/cli.cjs"
28
+ },
29
+ "dependencies": {
30
+ "@nicemeta/file-manager-lib": "^0.6.4",
31
+ "express": "^5.1.0",
32
+ "js-yaml": "^4.1.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ }
37
+ }
package/src/app.js ADDED
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+ const { createRouter } = require('@nicemeta/file-manager-lib');
7
+
8
+ // ── OpenAPI spec ──
9
+ // In dev: loaded from openapi.yaml via js-yaml (optional dev dependency)
10
+ // In prod bundle: loaded from openapi.json (pre-converted by build.js)
11
+ function loadSpec() {
12
+ const jsonPath = path.join(__dirname, 'openapi.json');
13
+ if (fs.existsSync(jsonPath)) {
14
+ return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
15
+ }
16
+ // Dev fallback: parse YAML
17
+ const yamlPath = path.join(__dirname, 'openapi.yaml');
18
+ const yaml = require('js-yaml');
19
+ return yaml.load(fs.readFileSync(yamlPath, 'utf8'));
20
+ }
21
+
22
+ /**
23
+ * Create the full Express application.
24
+ *
25
+ * @param {object} [options]
26
+ * @param {string} [options.root='/'] — root directory for file operations
27
+ * @returns {import('express').Application}
28
+ */
29
+ function createApp(options = {}) {
30
+ const app = express();
31
+
32
+ // Determine where static UI assets are:
33
+ // In dev: lib's own static/file-manager/ (resolved by the lib via __dirname)
34
+ // In prod bundle: dist/lib-static/file-manager/ (next to the bundle)
35
+ const libStaticDir = path.join(__dirname, 'lib-static', 'file-manager');
36
+ const routerOptions = { ...options };
37
+ if (fs.existsSync(libStaticDir)) {
38
+ routerOptions.staticDir = libStaticDir;
39
+ }
40
+
41
+ // Mount file-manager library router (API + static UI)
42
+ app.use(createRouter(routerOptions));
43
+
44
+ // Root redirect
45
+ app.get('/', (_req, res) => {
46
+ res.redirect('/file-manager/index.html');
47
+ });
48
+
49
+ // ── OpenAPI spec (JSON) ──
50
+ const specDoc = loadSpec();
51
+
52
+ app.get('/file-manager/api-docs/openapi.json', (_req, res) => {
53
+ res.json(specDoc);
54
+ });
55
+
56
+ // ── Swagger UI (CDN-loaded, dark theme inlined) ──
57
+ // Try multiple locations: static/ next to __dirname, or ../static/ for dev
58
+ const candidates = [
59
+ path.join(__dirname, 'static', 'swagger-ui.html'),
60
+ path.join(__dirname, '..', 'static', 'swagger-ui.html'),
61
+ ];
62
+ const swaggerHtmlPath = candidates.find(p => fs.existsSync(p));
63
+ const swaggerHtml = swaggerHtmlPath
64
+ ? fs.readFileSync(swaggerHtmlPath, 'utf8')
65
+ : null;
66
+
67
+ app.get('/file-manager/swagger-ui/index.html', (_req, res) => {
68
+ if (swaggerHtml) {
69
+ res.type('html').send(swaggerHtml);
70
+ } else {
71
+ res.status(404).send('swagger-ui.html not found');
72
+ }
73
+ });
74
+
75
+ // Redirect bare /file-manager/swagger-ui to /file-manager/swagger-ui/index.html
76
+ app.get('/file-manager/swagger-ui', (_req, res) => {
77
+ res.redirect('/file-manager/swagger-ui/index.html');
78
+ });
79
+
80
+ return app;
81
+ }
82
+
83
+ module.exports = { createApp };
84
+
@@ -0,0 +1,219 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: File Manager API
4
+ description: File system browsing, upload, download, and management
5
+ version: 0.1.0
6
+ servers:
7
+ - url: /
8
+ tags:
9
+ - name: File Manager
10
+ description: File system browsing, upload, download, and management
11
+ paths:
12
+ /file-manager/api/list:
13
+ get:
14
+ tags: [File Manager]
15
+ summary: List directory contents
16
+ description: Returns the contents of the specified directory including file metadata
17
+ parameters:
18
+ - name: path
19
+ in: query
20
+ description: Absolute path of the directory to list
21
+ schema:
22
+ type: string
23
+ default: /
24
+ responses:
25
+ '200':
26
+ description: Directory listing
27
+ content:
28
+ application/json:
29
+ schema:
30
+ $ref: '#/components/schemas/DirectoryInfo'
31
+
32
+ /file-manager/api/download:
33
+ get:
34
+ tags: [File Manager]
35
+ summary: Download a file
36
+ description: Downloads the specified file as an attachment
37
+ parameters:
38
+ - name: path
39
+ in: query
40
+ required: true
41
+ description: Absolute path of the file to download
42
+ schema:
43
+ type: string
44
+ responses:
45
+ '200':
46
+ description: File content
47
+ content:
48
+ application/octet-stream:
49
+ schema:
50
+ type: string
51
+ format: binary
52
+ '400':
53
+ description: Path is not a file
54
+
55
+ /file-manager/api/zip:
56
+ get:
57
+ tags: [File Manager]
58
+ summary: Download as ZIP
59
+ description: Downloads the specified file or directory as a ZIP archive
60
+ parameters:
61
+ - name: path
62
+ in: query
63
+ required: true
64
+ description: Absolute path of the file or directory to zip
65
+ schema:
66
+ type: string
67
+ responses:
68
+ '200':
69
+ description: ZIP archive
70
+ content:
71
+ application/zip:
72
+ schema:
73
+ type: string
74
+ format: binary
75
+
76
+ /file-manager/api/view:
77
+ get:
78
+ tags: [File Manager]
79
+ summary: View a file inline
80
+ description: Returns the file content for inline viewing in the browser
81
+ parameters:
82
+ - name: path
83
+ in: query
84
+ required: true
85
+ description: Absolute path of the file to view
86
+ schema:
87
+ type: string
88
+ responses:
89
+ '200':
90
+ description: File content (inline)
91
+ content:
92
+ application/octet-stream:
93
+ schema:
94
+ type: string
95
+ format: binary
96
+ '400':
97
+ description: Path is not a file
98
+
99
+ /file-manager/api/delete:
100
+ delete:
101
+ tags: [File Manager]
102
+ summary: Delete a file or directory
103
+ description: Recursively deletes the specified file or directory
104
+ parameters:
105
+ - name: path
106
+ in: query
107
+ required: true
108
+ description: Absolute path of the file or directory to delete
109
+ schema:
110
+ type: string
111
+ responses:
112
+ '204':
113
+ description: Successfully deleted
114
+
115
+ /file-manager/api/upload:
116
+ post:
117
+ tags: [File Manager]
118
+ summary: Upload files to a directory
119
+ description: Uploads one or more files to the specified directory
120
+ parameters:
121
+ - name: path
122
+ in: query
123
+ required: true
124
+ description: Absolute path of the target directory
125
+ schema:
126
+ type: string
127
+ requestBody:
128
+ required: true
129
+ content:
130
+ multipart/form-data:
131
+ schema:
132
+ type: object
133
+ properties:
134
+ file:
135
+ type: array
136
+ items:
137
+ type: string
138
+ format: binary
139
+ responses:
140
+ '200':
141
+ description: Files uploaded successfully
142
+
143
+ /file-manager/api/mode:
144
+ put:
145
+ tags: [File Manager]
146
+ summary: Change file permissions
147
+ description: "Changes read/write/execute permissions. Mode format: +rwx or -rwx"
148
+ parameters:
149
+ - name: path
150
+ in: query
151
+ required: true
152
+ description: Absolute path of the file
153
+ schema:
154
+ type: string
155
+ - name: mode
156
+ in: query
157
+ required: true
158
+ description: "Permission mode (e.g. +rw, -x)"
159
+ schema:
160
+ type: string
161
+ responses:
162
+ '200':
163
+ description: Permissions changed
164
+
165
+ components:
166
+ schemas:
167
+ DirectoryInfo:
168
+ type: object
169
+ properties:
170
+ name:
171
+ type: string
172
+ path:
173
+ type: string
174
+ isWritable:
175
+ type: boolean
176
+ parentInfos:
177
+ type: array
178
+ items:
179
+ $ref: '#/components/schemas/ParentInfo'
180
+ files:
181
+ type: array
182
+ items:
183
+ $ref: '#/components/schemas/FileInfo'
184
+
185
+ ParentInfo:
186
+ type: object
187
+ properties:
188
+ path:
189
+ type: string
190
+ name:
191
+ type: string
192
+
193
+ FileInfo:
194
+ type: object
195
+ properties:
196
+ parent:
197
+ type: string
198
+ name:
199
+ type: string
200
+ length:
201
+ type: integer
202
+ format: int64
203
+ isDirectory:
204
+ type: boolean
205
+ isReadable:
206
+ type: boolean
207
+ isWritable:
208
+ type: boolean
209
+ isExecutable:
210
+ type: boolean
211
+ hasChildren:
212
+ type: boolean
213
+ modified:
214
+ type: string
215
+ description: ISO local date-time (no timezone)
216
+ created:
217
+ type: string
218
+ description: ISO local date-time (no timezone)
219
+