# ftp

> Connect to an FTP server to list, upload, download, rename and delete remote files and folders.

This is the Markdown version of https://osl-lang.org/packages/ftp. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:ftp"
```

FTP is the old protocol for moving files to and from a server, still common on shared hosting and in data feeds. `std:ftp` connects, logs in, and gives you a remote file manager. For servers you control, prefer [ssh](/packages/ssh.md) with SFTP.

## Connect

`ftp.connect(address, options)` opens a connection and logs in. It returns a result. Without a `username` it logs in as `anonymous`. Close the connection when you are done:

```osl
import "std:ftp"
import "std:env"

auto conn = ftp.connect("ftp.example.com", {
  username: "deploy",
  password: env.get("FTP_PASSWORD")
}).unwrap()

log conn.cwd().unwrap()
conn.close().unwrap()
```

TLS is on by default (`tls: "auto"` switches to it when the server offers). `"starttls"` requires it, `"implicit"` speaks it from the first byte, and `"none"` never uses it.

## Your password is never sent in the clear

A login to another machine without TLS is refused before the password leaves your program. You can see this safely, because it fails before any data is sent:

```osl
import "std:ftp"

auto attempt = ftp.connect("203.0.113.1:21", {
  username: "u",
  password: "p",
  tls: "none",
  timeout: 500
})
log attempt.unwrapErr().kind
log attempt.unwrapErr().message
```

Output:

```text
UnsupportedError
ftp.connect: 203.0.113.1:21 has no TLS, so the password would be sent unencrypted; use a server that offers TLS, or tls "implicit"
```

Loopback addresses are exempt, and so is the anonymous login. Change `tls: "none"` to `tls: "auto"` and run again: now it tries to connect, and fails with a `NetworkError` or `TimeoutError` because nothing is there.

## Connection errors

A refused or unreachable server is a `NetworkError`, and a slow one a `TimeoutError`:

```osl
import "std:ftp"

auto attempt = ftp.connect("127.0.0.1:1", {timeout: 1000})
log attempt.isErr()
log attempt.unwrapErr().kind
```

Output:

```text
true
NetworkError
```

A refused login is a `PermissionError`.

## Look around

`entries(path)` lists a folder, `stat` describes one item, and `exists`, `size` and `cwd` answer smaller questions. Paths are the server's:

```osl
import "std:ftp"

auto conn = ftp.connect("ftp.example.com", {username: "deploy", password: "..."}).unwrap()

for entry in conn.entries("/uploads").unwrap() (
  string kind = if entry.isDir then "dir " else "file"
  log kind entry.name entry.size
)
log conn.exists("/uploads/report.csv")
log conn.size("/uploads/report.csv").unwrapOr(-1)
conn.close().unwrap()
```

## Upload and download

`upload(local, remote)` replaces the remote file, and `download(remote, local)` writes a local one. `uploadDir` and `downloadDir` copy a whole folder:

```osl
import "std:ftp"
import "std:fs"

fs.writeFile("report.csv", "a,b\n1,2\n").unwrap()

auto conn = ftp.connect("ftp.example.com", {username: "deploy", password: "..."}).unwrap()
conn.upload("report.csv", "/uploads/report.csv").unwrap()
conn.download("/uploads/report.csv", "copy.csv").unwrap()
log fs.readFile("copy.csv").unwrap()
conn.close().unwrap()
```

Missing remote files fail with a `NotFoundError`, so check the result and not just the call.

## Rename, make folders, delete

```osl
import "std:ftp"

auto conn = ftp.connect("ftp.example.com", {username: "deploy", password: "..."}).unwrap()

conn.mkdir("/archive").unwrap()
conn.rename("/uploads/report.csv", "/archive/report.csv").unwrap()
conn.remove("/archive/old.csv").unwrap()
conn.removeDir("/empty-folder").unwrap()
conn.close().unwrap()
```

`mkdir` needs the parent to exist, and `removeDir` needs the folder to be empty.

**Quiz.** You connect to a remote host with a username, a password and `tls: "none"`. What happens?

- [ ] It logs in and sends the password as plain text.
- [x] It fails with an `UnsupportedError` before the password is sent.
- [ ] It silently turns TLS on.

Answer: `std:ftp` refuses to send a password unencrypted to any machine but your own.

### Exercise: Handle a dead server

Try to connect to `127.0.0.1:1` with a 500 ms timeout. Print `"offline"` when it fails and the error kind after it.

Starting code:

```osl
import "std:ftp"

log "?"
```

It should print:

```text
offline NetworkError
```

Hint 1: `ftp.connect("127.0.0.1:1", {timeout: 500})` returns a result.

Hint 2: `switch` with `case ok(conn)` and `case err(e)`.

A solution:

```osl
import "std:ftp"

switch ftp.connect("127.0.0.1:1", {timeout: 500}) (
  case ok(conn)
    log "online"
    conn.close().unwrap()
  case err(e)
    log "offline" e.kind
)
```

## API reference

## API reference

Every member of `std:ftp`, generated from the source of OSL.

### Functions

#### `ftp.changeDirectory(any path) boolean`

**Deprecated.** Deprecated: use client.chdir(path) instead.

changeDirectory always returns false.

#### `ftp.connect(string address, any ...options) result<*ftp.FTP, error>`

connect opens and logs in to an FTP server. Options: username (default
"anonymous"), password, timeout (milliseconds, default 10000) and tls,
as in std:email: "auto" (the default) switches to TLS with AUTH TLS when
the server offers it, "starttls" requires that, "implicit" speaks TLS
from the start (port 990 unless address names one) and "none" never uses
TLS. A password is never sent unencrypted to another machine: without
TLS, a login to a host that isn't localhost or a loopback address fails
with UnsupportedError before the password is sent, unless it is the
anonymous login (user "anonymous" with no password or the default one). Also fails with PermissionError, NetworkError or TimeoutError. The
deprecated four-argument form returns a disconnected client instead of
failing.

#### `ftp.connectEx(any host, any port, any user, any ...rest) *ftp.FTP`

**Deprecated.** Deprecated: use ftp.connect(address, {username, password}) instead, which fails with a typed error.

connectEx(host, port, user, password) connects and logs in, or returns
null on any failure.

#### `ftp.createDirectory(any path) boolean`

**Deprecated.** Deprecated: use client.mkdir(path) instead.

createDirectory always returns false.

#### `ftp.currentDirectory() string`

**Deprecated.** Deprecated: use client.cwd() instead.

currentDirectory always returns "".

#### `ftp.delete(any path) boolean`

**Deprecated.** Deprecated: use client.remove(path) instead.

delete always returns false.

#### `ftp.deleteDirectory(any path) boolean`

**Deprecated.** Deprecated: use client.removeDir(path) instead.

deleteDirectory always returns false.

#### `ftp.disconnect() boolean`

**Deprecated.** Deprecated: use client.close() instead.

disconnect always returns false.

#### `ftp.download(any remotePath, any localPath) boolean`

**Deprecated.** Deprecated: use client.download(remotePath, localPath) instead.

download always returns false.

#### `ftp.downloadDirectory(any remoteDir, any localDir) boolean`

**Deprecated.** Deprecated: use client.downloadDir(remoteDir, localDir) instead.

downloadDirectory always returns false.

#### `ftp.exists(any path) boolean`

**Deprecated.** Deprecated: use client.exists(path) instead.

exists always returns false.

#### `ftp.getFileSize(any path) int64`

**Deprecated.** Deprecated: use client.size(path) instead.

getFileSize always returns 0.

#### `ftp.getStatistics() object`

**Deprecated.** Deprecated: use client.isConnected() instead.

getStatistics always returns {}.

#### `ftp.isActive() boolean`

**Deprecated.** Deprecated: use client.isConnected() instead.

isActive always returns false.

#### `ftp.list(any path) array`

**Deprecated.** Deprecated: use client.entries(path) instead.

list always returns [].

#### `ftp.login() boolean`

**Deprecated.** Deprecated: use ftp.connect(address, options) instead.

login always returns false.

#### `ftp.passiveMode(any enabled) result<unit, error>`

**Deprecated.** Deprecated: transfers always use passive mode.

passiveMode succeeds for true and fails with UnsupportedError for false.

#### `ftp.rename(any oldPath, any newPath) boolean`

**Deprecated.** Deprecated: use client.rename(from, to) instead.

rename always returns false.

#### `ftp.setMode(any path, any mode) result<unit, error>`

**Deprecated.** Deprecated: FTP has no portable way to change permissions.

setMode fails with UnsupportedError.

#### `ftp.setModificationTime(any path, any timestamp) boolean`

**Deprecated.** Deprecated: use client.setModTime(path, unixMilliseconds) instead.

setModificationTime always returns false.

#### `ftp.setTimeout(any seconds) result<unit, error>`

**Deprecated.** Deprecated: pass {timeout: milliseconds} to ftp.connect instead.

setTimeout fails with UnsupportedError.

#### `ftp.sync(any localDir, any remoteDir) boolean`

**Deprecated.** Deprecated: use client.uploadDir(localDir, remoteDir) instead.

sync always returns false.

#### `ftp.upload(any localFile, any remotePath) boolean`

**Deprecated.** Deprecated: use client.upload(localPath, remotePath) instead.

upload always returns false.

#### `ftp.uploadDirectory(any localDir, any remoteDir) boolean`

**Deprecated.** Deprecated: use client.uploadDir(localDir, remoteDir) instead.

uploadDirectory always returns false.

### `*ftp.FTP`

#### `fTP.changeDirectory(any path) boolean`

**Deprecated.** Deprecated: use client.chdir(path) instead.

changeDirectory changes the remote working directory, returning false on
failure.

#### `fTP.chdir(string path) result<unit, error>`

chdir changes the remote working directory, which relative paths start
from. Fails with NotFoundError when path doesn't exist.

#### `fTP.close() result<unit, error>`

close logs out and closes the connection. Closing twice is a no-op. Fails
with NetworkError when the server can't be told, but the connection is
closed either way.

#### `fTP.connectEx(any host, any port, any user, any ...rest) *ftp.FTP`

**Deprecated.** Deprecated: use ftp.connect(address, {username, password}) instead.

connectEx connects to another server, or returns null on failure.

#### `fTP.createDirectory(any path) boolean`

**Deprecated.** Deprecated: use client.mkdir(path) instead.

createDirectory creates a remote directory, returning false on failure.

#### `fTP.currentDirectory() string`

**Deprecated.** Deprecated: use client.cwd() instead.

currentDirectory returns the remote working directory, or "" on failure.

#### `fTP.cwd() result<string, error>`

cwd returns the remote working directory, such as "/uploads".

#### `fTP.delete(any path) boolean`

**Deprecated.** Deprecated: use client.remove(path) instead.

delete removes a remote file, returning false on failure.

#### `fTP.deleteDirectory(any path) boolean`

**Deprecated.** Deprecated: use client.removeDir(path) instead.

deleteDirectory removes an empty remote directory, returning false on
failure.

#### `fTP.disconnect() boolean`

**Deprecated.** Deprecated: use client.close() instead.

disconnect logs out, returning false when the client wasn't connected.

#### `fTP.download(string remotePath, string localPath) result<unit, error>`

download copies the remote file at remotePath to localPath, creating
missing local directories. A partly written file is removed. Fails with
NotFoundError when the remote file doesn't exist, IOError when the
transfer breaks off, and PermissionError or IOError when localPath can't
be written.

#### `fTP.downloadDir(string remoteDir, string localDir) result<unit, error>`

downloadDir copies the remote directory remoteDir and everything in it to
localDir. Fails like download, and with IOError when the server lists an
unsafe name or a symlink.

#### `fTP.downloadDirectory(any remoteDir, any localDir) boolean`

**Deprecated.** Deprecated: use client.downloadDir(remoteDir, localDir) instead.

downloadDirectory downloads a directory, returning false on failure.

#### `fTP.entries(string path) result<{isDir: boolean, isSymlink: boolean, modTime: int?, name: string, path: string, size: int, target: string?}[], error>`

entries lists the directory at path ("" for the current directory). Each
entry has the keys name, path, isDir, isSymlink, size (bytes), modTime
(unix milliseconds, null when the server doesn't say) and target (where a
symlink points, null otherwise). "." and ".." are left out. Fails with
NotFoundError when the directory doesn't exist.

#### `fTP.exists(string path) boolean`

exists reports whether a remote file or directory exists at path.

#### `fTP.getFileSize(any path) int64`

**Deprecated.** Deprecated: use client.size(path) instead.

getFileSize returns a remote file's size, or 0 on failure.

#### `fTP.getStatistics() object`

**Deprecated.** Deprecated: use client.isConnected() instead.

getStatistics returns {host, port, user, connected}, or {} when not
connected.

#### `fTP.isActive() boolean`

**Deprecated.** Deprecated: use client.isConnected() instead.

isActive reports whether the client is connected.

#### `fTP.isConnected() boolean`

isConnected reports whether the client is logged in and not closed. It
does not contact the server.

#### `fTP.list(any path) array`

**Deprecated.** Deprecated: use client.entries(path) instead.

list returns the entries of a directory as {name, size, isDir, type,
target, time}, or [] on failure.

#### `fTP.login() boolean`

**Deprecated.** Deprecated: connect logs in; use client.isConnected() instead.

login checks that the connection is alive with a NOOP; connect has
already logged in.

#### `fTP.mkdir(string path) result<unit, error>`

mkdir creates the remote directory path. Its parent must exist. Fails
with PermissionError when the server refuses, which most servers also do
when path already exists.

#### `fTP.passiveMode(any enabled) result<unit, error>`

**Deprecated.** Deprecated: transfers always use passive mode.

passiveMode succeeds for true, since transfers always use passive mode,
and fails with UnsupportedError for false.

#### `fTP.remove(string path) result<unit, error>`

remove deletes the remote file at path. Fails with NotFoundError when it
doesn't exist.

#### `fTP.removeDir(string path, object? options) result<unit, error>`

removeDir deletes the remote directory path, which must be empty.
Options: recursive (boolean, default false) deletes everything in it
first. Fails with PermissionError when the server refuses, which servers
answer alike for a directory that is missing or not empty.

#### `fTP.rename(string from, string to) result<unit, error>`

rename moves or renames the remote file or directory from to to. Fails
with NotFoundError when from doesn't exist.

#### `fTP.setModTime(string path, int modTime) result<unit, error>`

setModTime sets the modification time of the remote file at path to
modTime (unix milliseconds; servers keep whole seconds). Fails with
UnsupportedError when the server can't set times (no MFMT) and
NotFoundError when the file doesn't exist.

#### `fTP.setMode(any path, any mode) result<unit, error>`

**Deprecated.** Deprecated: FTP has no portable way to change permissions.

setMode never changed permissions; it now fails with UnsupportedError.

#### `fTP.setModificationTime(any path, any timestamp) boolean`

**Deprecated.** Deprecated: use client.setModTime(path, unixMilliseconds) instead.

setModificationTime sets a remote file's time from an RFC 3339 string or
unix seconds, returning false on failure.

#### `fTP.setTimeout(any seconds) result<unit, error>`

**Deprecated.** Deprecated: pass {timeout: milliseconds} to ftp.connect instead.

setTimeout never changed anything once connected; it now says so.

#### `fTP.size(string path) result<int, error>`

size returns the size in bytes of the remote file at path. Fails with
NotFoundError when it doesn't exist.

#### `fTP.stat(string path) result<{isDir: boolean, isSymlink: boolean, modTime: int?, name: string, path: string, size: int, target: string?}, error>`

stat describes the remote file or directory at path, with the same keys
as an entries entry. Fails with NotFoundError when nothing is there and
UnsupportedError when the server can't describe single files (no MLST).

#### `fTP.sync(any localDir, any remoteDir) boolean`

**Deprecated.** Deprecated: use client.uploadDir(localDir, remoteDir) instead.

sync uploads a directory, returning false on failure.

#### `fTP.upload(string localPath, string remotePath) result<unit, error>`

upload copies the local file at localPath to remotePath, replacing it.
Fails with NotFoundError or PermissionError when the local file can't be
read, IOError when localPath isn't a regular file, and PermissionError
when the server refuses the write.

#### `fTP.uploadDir(string localDir, string remoteDir) result<unit, error>`

uploadDir copies the local directory localDir and everything in it to
remoteDir, creating remote directories as needed (existing ones are
reused). Fails like upload, and with IOError when localDir isn't a
directory or contains a symlink.

#### `fTP.uploadDirectory(any localDir, any remoteDir) boolean`

**Deprecated.** Deprecated: use client.uploadDir(localDir, remoteDir) instead.

uploadDirectory uploads a directory, returning false on failure.
