feat/files-commands#39
Open
mateuscardosodeveloper wants to merge 19 commits into
Open
Conversation
Pull collectFiles and the throttled upload queue out of the backup restore path into lib/upload-folder.ts, so the upcoming files-upload command and backup restore share one implementation. - uploadFiles(): throttled (concurrency 2, 300ms) upload of a pre-collected file list; per-file failure is counted, not fatal. - uploadFolder(): sugar over collectFiles + uploadFiles for the whole-folder case, with a remotePath prefix. - restoreFiles() now consumes the helper, keeping granular selection and spinner behavior unchanged.
Upload a local file or folder to TagoIO Files under a remote path prefix, through the shared upload-folder helper (throttled, rate-limit aware). Resolves the account via getEnvironmentConfig and honors --token for CI/CD. remotePath defaults to the basename of localPath; --public makes every uploaded file public. Generic by design: no special-casing of index.html or widgets.
Print the URL of a file already in TagoIO Files. The URL is written to stdout alone so it can be captured in scripts; --signed returns a signed URL via the SDK (the built URL is passed to getFileURLSigned). The base URL is resolved from the profile region via a new getApiURL helper, so it is correct for named regions (us-e1, eu-w1) and for custom regions (TagoDeploy, self-hosted) instead of assuming a single endpoint.
Set the URL on a custom (iframe) widget. Reads the widget first and merges the new url into display, so existing parameters, theme, frame settings, and header buttons are preserved (read-modify-write). Rejects non-iframe widgets, since only custom widgets carry a URL. Uses the Resources class for consistency with the files commands.
Wire files-upload, files-url, and widget-edit-url into buildProgram so they appear in 'tagoio --help' and the generated man page, each under its own section header with usage examples. Refreshes the man snapshot.
uploadFolder assumed localPath was always a directory and called collectFiles, which crashed with ENOTDIR on a single file. Detect file vs directory: a single file becomes one task with an empty relative path so it lands at the full remotePath. Adds unit tests for the single-file and missing-path cases.
Widget editing leaves the CLI: pointing a custom widget at a URL is one narrow slice of widget management, and a single-field command would lock the CLI into it. The consuming project (template-analysis) now edits the widget via the SDK instead. The CLI keeps only the generic Files primitives (files-upload, files-url). Refreshes the man snapshot.
uploadBase64 cannot set a Content-Type, so uploaded files landed in storage as application/octet-stream and browsers downloaded them instead of rendering — which broke custom-widget index.html in the dashboard iframe. Switch to uploadFile (multipart), which accepts a contentType, derived from the file extension (html/js/css/svg/fonts/etc., defaulting to octet-stream). Verified against storage: index.html serves as text/html, JS as text/javascript, CSS as text/css.
lib/files-paths.ts centralizes the logic the folder-capable files-* commands share: isFolderPath (trailing-slash or no-extension detection), listFilesRecursive (flatten a prefix's files across nested folders via files.list), and remapPrefix (rewrite a file's from-prefix to to- for move/copy of a folder). Pure and unit-tested so move/copy/delete/ download/permission stay thin over one implementation.
List folders and files under a path in TagoIO Files (root if omitted). --json emits a machine-readable object on stdout; plain output prints folders (with a trailing slash) then files. Reuses the env/token resolution shared by the other files-* commands.
Move a file or a folder prefix. A single file is one move call; a folder is listed recursively (lib/files-paths) and each file moved with its prefix remapped, throttled through the shared queue. Folder moves of >1 file confirm unless --yes/--silent. Exports executeMove and resolveResources for reuse by files-rename. Errors on an empty prefix.
Rename a file or folder in place: keep the parent directory, change only the last segment. Reuses executeMove from files-move (file: one move; folder: list + remap + move each). Rejects a newName containing '/', pointing to files-move for cross-directory moves. Folder renames of >1 file confirm unless --yes.
Copy a file or folder prefix. Single file is one copy call; a folder is listed recursively and each file copied with its prefix remapped, throttled through the shared queue. No confirmation (copy is non-destructive). Reuses resolveResources from files-move. Errors on an empty prefix.
Delete a file or every file under a folder prefix. Always confirms (folder shows the count) unless --yes/--silent. Folder deletes list recursively then delete in batches of 50, throttled between batches. No-op with a clear message when nothing matches; never reports a partial delete as success.
Download a file or folder prefix to the local disk. Resolves each file's URL from the profile region (getApiURL), signing it via getFileURLSigned when the file is private (checkPermission), then fetches and writes it, creating parent dirs. Folder downloads list recursively and preserve the relative structure under the destination (default: the basename in cwd). Errors on an empty prefix or a failed fetch.
Make a file or folder prefix public or private via changePermission. Validates the visibility argument (public/private). Single file is one call; a folder lists recursively and changes each file, throttled. Folder changes of >1 file confirm unless --yes/--silent. Errors on an empty prefix.
Wire files-list, files-move, files-rename, files-copy, files-delete, files-download, and files-permission into the files namespace with --help examples (move/rename cover file and folder). Refresh the man snapshot. Completes the Files management surface alongside the Phase 1 files-upload/files-url.
End-to-end testing against a real profile revealed the TagoIO Files API only returns a folder's contents when the path ends with a slash; without it the folder comes back as a sibling with no contents. This broke files-list (showed the folder instead of its files) and made listFilesRecursive loop/return empty — hanging folder move/copy/delete/ permission/download. Two fixes: - listFilesRecursive normalizes the prefix with a trailing slash and joins the relative folder names from the API before recursing. - files-list appends a slash to a non-empty path. Unit tests updated to the real API contract (relative folder names, slash-terminated listing).
Review follow-ups: - I2: extract resolveResources from move.ts into lib/resolve-resources.ts (with its own tests) and have all files-* commands import it from there, removing the odd coupling where copy/delete/permission depended on the move module. It now also returns the region for URL-building callers. - I1: files-list and files-download reuse resolveResources instead of reimplementing the env/token resolution. - C1: files-download guards each folder write with safeJoin, refusing any path that resolves outside the destination (path-traversal protection). Re-validated end to end against a real profile: list, private-folder download (signed URL), and permission still work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does PR do?
Adds a complete Files management command surface to the TagoIO CLI, plus the generic primitives a project needs to deploy and locally serve custom widgets without touching the web UI.
📁 Files manager
Nine
files-*commands over TagoIO Files, all sharing one env/token resolution and a throttled queue (rate-limit aware):Content-Typeper extension (soindex.htmlrenders in an iframe instead of downloading)--signed), region-aware, capturable in scripts--json)--yesto skip)🗂️ Folder semantics
Folder operations list a prefix recursively and act on each file, reusing a shared
lib/files-pathshelper. The TagoIO API only lists a folder's contents with a trailing slash — handled centrally.🔒 Safety
--yeslib/resolve-resources;--tokensupported for CI/CD🧪 Quality