diff --git a/README.md b/README.md index 7722a8e..ae39721 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Run `onedrive help` (or `-h`/`--help`) for this list at any time, and - `mcp` - run a read-only [MCP](https://modelcontextprotocol.io) server over stdio - `mkdir` - create a remote folder - `mv` - move a local file to OneDrive or vice-versa +- `realpath` - print the View Online URL of a remote or locally synced file - `rm` - delete a file or folder from OneDrive - `sendmail` - send an invitation email for editing to recipients - `stat` - dump all information for particular file(s) @@ -90,6 +91,20 @@ Run `onedrive help` (or `-h`/`--help`) for this list at any time, and `onedrive find 'Pictures/Camera Roll' -regex 2015 -type f -print0 | xargs -0 onedrive mv -t :/Pictures/2015/` +##### Get the web address of a file + +`realpath` prints the item's own _View Online_ URL, which exists whether or not +the file has ever been shared. It accepts a remote path, or a local path inside +the folder the OneDrive sync client mirrors: + +```sh +onedrive realpath Documents/report.docx +onedrive realpath ~/OneDrive/Documents/report.docx +``` + +To hand someone else access instead, use `ln`: that creates a new sharing +permission and prints _its_ link, which `chmod` can later downgrade or revoke. + ##### Create an album and add photos to it ```sh @@ -185,6 +200,7 @@ so this stays a pointer rather than a second source of truth: ## DONE +- Print the View Online URL of a file (`onedrive realpath`) - Read-only [MCP](https://modelcontextprotocol.io) server (`onedrive mcp`) - Photo albums via bundles (`onedrive album`) - Full-text search across the drive (`onedrive grep`) diff --git a/bin/onedrive b/bin/onedrive index 183d444..f3ea989 100755 --- a/bin/onedrive +++ b/bin/onedrive @@ -1048,6 +1048,76 @@ async function stat(args) { } } +// OneDrive's sync clients mirror the drive into one of a handful of well-known +// directories. The `OneDrive*` variables are set by the Windows client; the +// rest are the defaults elsewhere. +async function syncRoots() { + const home = require('os').homedir() + const roots = [ + process.env.OneDrive, + process.env.OneDriveConsumer, + process.env.OneDriveCommercial, + Path.join(home, 'OneDrive'), + ].filter(Boolean) + // macOS mounts the drive under ~/Library/CloudStorage as OneDrive-Personal, + // or OneDrive- for work accounts, so the leaf name is not fixed. + const cloud = Path.join(home, 'Library', 'CloudStorage') + try { + const names = await FS.readdir(cloud) + roots.push( + ...names + .filter(name => name.startsWith('OneDrive')) + .map(name => Path.join(cloud, name)) + ) + } catch { + // No ~/Library/CloudStorage: not a Mac, or nothing is mounted there. + } + return roots +} + +// Compared case-insensitively: every platform running the sync client has a +// case-insensitive filesystem by default. +function stripPrefix(path, root) { + const prefix = root.endsWith(Path.sep) ? root : root + Path.sep + return path.toLowerCase().startsWith(prefix.toLowerCase()) + ? path.slice(prefix.length) + : undefined +} + +// Only an explicitly local-looking path can name a file in the sync folder, so +// a bare `Documents/report.docx` stays remote the way it is for every other +// command. An absolute path that is not inside a sync folder stays remote too. +async function localToRemote(file) { + if (!/^([~.]|\/|\\|[A-Za-z]:)/.test(file)) { + return undefined + } + const home = require('os').homedir() + const abs = Path.resolve(file.replace(/^~(?=[/\\]|$)/, home)) + for (const root of await syncRoots()) { + const rel = stripPrefix(abs, Path.resolve(root)) + if (rel !== undefined) { + return rel.split(Path.sep).join('/') + } + } + return undefined +} + +async function realpath1(file) { + // `webUrl` is the item's own address and exists whether or not it has ever + // been shared -- unlike the sharing link `ln` mints (#12). + const result = await getItem((await localToRemote(file)) ?? file) + console.log(Colors.underline(result.webUrl)) +} + +async function realpath(args) { + if (args.length === 0) { + args = [''] + } + for (const cur of args) { + await realpath1(cur) + } +} + async function find_paged(cont, expression) { if (cont === undefined) { return @@ -1711,7 +1781,7 @@ async function main(argv) { console.log( '\nusage: ' + commandName() + - ' COMMAND [arguments]\n\ncommands: album albums cat chmod cp df find grep help ln login ls mcp mkdir mv rm sendmail stat wget' + ' COMMAND [arguments]\n\ncommands: album albums cat chmod cp df find grep help ln login ls mcp mkdir mv realpath rm sendmail stat wget' ) return @@ -1762,6 +1832,9 @@ async function main(argv) { case 'mv': return mv(argv.slice(3)) + case 'realpath': + return realpath(argv.slice(3)) + case 'rm': return rm(argv.slice(3)) diff --git a/docs/index.html b/docs/index.html index e964fbd..855e2cf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -493,6 +493,7 @@

Usage

  • mcp - run a read-only MCP server over stdio
  • mkdir - create a remote folder
  • mv - move a local file to OneDrive or vice-versa
  • +
  • realpath - print the View Online URL of a remote or locally synced file
  • rm - delete a file or folder from OneDrive
  • sendmail - send an invitation email for editing to recipients
  • stat - dump all information for particular file(s)
  • @@ -521,6 +522,19 @@
    Move remote files to a new folder

    onedrive find 'Pictures/Camera Roll' -regex 2015 -type f -print0 | xargs -0 onedrive mv -t :/Pictures/2015/

    +
    Get the web address of a file
    + +

    realpath prints the item's own View Online URL, which exists whether or not +the file has ever been shared. It accepts a remote path, or a local path inside +the folder the OneDrive sync client mirrors:

    + +
    onedrive realpath Documents/report.docx
    +onedrive realpath ~/OneDrive/Documents/report.docx
    +
    + +

    To hand someone else access instead, use ln: that creates a new sharing +permission and prints its link, which chmod can later downgrade or revoke.

    +
    Create an album and add photos to it
    onedrive album create 'Summer 2026'