Files
Medios-Macina/docs/ftp_plugin_tutorial.md
T
2026-05-03 21:20:05 -07:00

5.3 KiB

FTP Plugin Walkthrough

This walkthrough adds a real bundled ftp plugin so users can:

  • run search-file -plugin ftp -instance <name> ...
  • browse remote folders as result tables
  • select file rows to download-file
  • pipe selected file rows into add-file
  • upload local files with add-file -plugin ftp -instance <name>

The implementation lives in plugins/ftp/__init__.py.

What The Plugin Does

The FTP plugin demonstrates the main provider hooks that matter for a storage-style integration:

  • config_schema() exposes host, credentials, base path, TLS, and search depth.
  • extract_query_arguments() supports inline query fields like path: and depth:.
  • search() walks an FTP directory tree and returns SearchResult rows.
  • selector() turns folder rows into a follow-up table when the user runs @N.
  • download() and download_url() fetch FTP files into download-file output paths.
  • resolve_pipe_result_download() lets @N | add-file -instance ... materialize a remote FTP file first.
  • upload() lets add-file -plugin ftp -instance <name> -path ... push a local file to the configured FTP server.

Example Config

Add one or more named FTP provider instances to your config:

[provider.ftp.work]
host = "ftp.example.com"
port = 21
username = "demo"
password = "secret"
base_path = "/incoming"
tls = false
passive = true
timeout = 20
search_depth = 1

[provider.ftp.archive]
host = "archive.example.com"
port = 2121
username = "archive-bot"
password = "secret"
base_path = "/dropbox"
tls = true

Notes:

  • work and archive are instance names; use them with -instance work or -instance archive.
  • host is the only required field for each instance to validate.
  • username defaults to anonymous and password defaults to anonymous@.
  • base_path is both the default search root and the upload target directory.
  • search_depth controls how many folder levels search-file -plugin ftp scans by default.

Search Flow

Basic listing from the configured base path:

search-file -plugin ftp -instance work "*"

Search by filename fragment:

search-file -plugin ftp -instance work "invoice"

Search a different subtree and recurse deeper:

search-file -plugin ftp -instance work "path:/pub depth:2 invoice"

Filter to folders only:

search-file -plugin ftp -instance work "path:/pub type:folder *"

The plugin returns rows with explicit columns for name, type, directory, size, and modification time.

Selection Flow

Folder rows are navigation rows. If the selected row is a directory, plain @N opens a new FTP table for that directory:

search-file -plugin ftp -instance work "*"
@2

File rows carry an explicit row action:

download-file -plugin ftp -instance work -url ftp://ftp.example.com/incoming/report.pdf

That means plain @N on a file row downloads it immediately:

search-file -plugin ftp -instance work "report"
@1

Download And Add-File Flow

If you want the downloaded file in a specific local directory:

search-file -plugin ftp -instance work "report"
@1 | download-file -path C:\Downloads

If you want to ingest the selected FTP file into a configured instance backend:

search-file -plugin ftp -instance work "report"
@1 | add-file -instance tutorial

Why this works:

  • the file row advertises a download-file row action
  • the pipeline auto-inserts that download before add-file
  • the FTP plugin also implements resolve_pipe_result_download() so provider-owned FTP rows can be materialized for ingestion
  • file rows also carry the chosen instance, so selection replay and @N | add-file ... keep the same FTP target

Upload Flow

Uploading uses the same provider name, but through add-file -plugin ftp -instance <name>:

add-file -plugin ftp -instance archive -path C:\Media\report.pdf

That sends the file to the selected instance's FTP base_path and returns the FTP URL as the uploaded result.

Why The Row Metadata Matters

The critical part of this plugin is the file-row metadata:

  • file rows emit _selection_args as ['-instance', '<name>', '-url', '<ftp-url>']
  • file rows emit _selection_action as ['download-file', '-plugin', 'ftp', '-instance', '<name>', '-url', '<ftp-url>']
  • folder rows do not emit a download action, so selector() can own drill-in behavior instead

That split is what keeps these two user experiences compatible:

  • @N on a folder opens a new table
  • @N on a file downloads the file
  • @N | add-file -instance ... first downloads, then ingests

Implementation Notes

The plugin prefers MLSD for directory listings and falls back to NLST plus directory probes when the server does not support machine-readable listings.

The code is intentionally small and uses only Python stdlib pieces:

  • ftplib for FTP and FTPS
  • fnmatch for wildcard-style search tokens
  • tempfile for add-file handoff downloads
search-file -plugin ftp -instance work "*"
search-file -plugin ftp -instance work "path:/incoming depth:2 *.pdf"
@1
@1 | download-file -path C:\Downloads
@1 | add-file -instance tutorial
add-file -plugin ftp -instance archive -path C:\Media\report.pdf