// Copyright 2026 PaiGack
// Licensed under the Apache License, Version 2.0.
// Ported from jlaffaye/ftp (ISC License), see LICENSE-THIRD-PARTY.
// Layer: pure logic — no IO, no `moonbitlang/async` dependency.
///|
/// The kind of a remote directory entry, mirroring upstream
/// `ftp.EntryType`.
pub(all) enum EntryType {
/// A regular file.
File
/// A directory.
Folder
/// A symbolic link.
Link
} derive(Eq, @debug.Debug)
///|
/// Return the lowercase name of the entry type, matching the string form used
/// by the Go implementation (`file` / `folder` / `link`).
pub fn EntryType::to_string(self : EntryType) -> String {
match self {
File => "file"
Folder => "folder"
Link => "link"
}
}
///|
/// A single remote directory entry.
///
/// Mirroring upstream `ftp.Entry`:
/// - `target` is empty for non-symlinks (Go zero value semantics),
/// - `time` carries an explicit timezone because `LIST` timestamps are
/// wall-clock time in the **server's** timezone.
pub struct Entry {
/// File name without any leading path component.
mut name : String
/// Symbolic link target, empty string when the entry is not a link.
mut target : String
/// Kind of the entry.
mut type_ : EntryType
/// Size in bytes, `UInt64` because servers do report values above `Int64`.
mut size : UInt64
/// Parsed modification time, timezone included.
mut time : @time.ZonedDateTime
} derive(@debug.Debug)
///|
/// Build an entry with the given name and type; the target defaults to the
/// empty string and the time to the epoch in UTC, both of which the parsers
/// overwrite when the server supplies the information.
pub fn make_entry(
name : String,
type_ : EntryType,
time? : @time.ZonedDateTime,
) -> Entry {
let timestamp = match time {
Some(time) => time
None => epoch
}
{ name, target: "", type_, size: 0, time: timestamp, }
}
///|
/// The UNIX epoch, a convenient zero value for `Entry::time`.
///
/// `@time.ZonedDateTime::from_unix_second` raises, which is awkward in pure
/// parsers, so the fallback lives here where the failure path can be handled
/// exactly once.
pub let epoch : @time.ZonedDateTime = @time.ZonedDateTime::from_unix_second(
0,
zone=@time.utc_zone,
) catch {
_ => panic()
}
///|
/// Replace the name of an entry in place.
pub fn Entry::set_name(self : Entry, name : String) -> Unit {
self.name = name
}
///|
/// Replace the symlink target of an entry in place.
pub fn Entry::set_target(self : Entry, target : String) -> Unit {
self.target = target
}
///|
/// Replace the kind of an entry in place.
pub fn Entry::set_type(self : Entry, type_ : EntryType) -> Unit {
self.type_ = type_
}
///|
/// Replace the size of an entry in place.
pub fn Entry::set_size(self : Entry, size : UInt64) -> Unit {
self.size = size
}
///|
/// Replace the modification time of an entry in place.
pub fn Entry::set_time(self : Entry, time : @time.ZonedDateTime) -> Unit {
self.time = time
}
///|
/// Build a plain file entry, the most common shape.
pub fn make_file(name : String, time? : @time.ZonedDateTime) -> Entry {
make_entry(name, EntryType::File, time?)
}
///|
/// Parse a `TYPE` command argument back into a `TransferType`, the inverse of
/// `TransferType::to_string`.
pub fn TransferType::from_string(value : String) -> TransferType? {
match value.to_upper() {
"I" => Some(Binary)
"A" => Some(ASCII)
_ => None
}
}
///|
/// The FTP `TYPE` argument, an alias of `to_string` that reads better at the
/// call site.
pub fn TransferType::argument(self : TransferType) -> String {
self.to_string()
}
///|
/// Build a folder entry, the shape used by the `walker` root marker.
pub fn make_folder(name : String, time? : @time.ZonedDateTime) -> Entry {
make_entry(name, EntryType::Folder, time?)
}
///|
/// Build a link entry with the given target.
pub fn make_link(
name : String,
target : String,
time? : @time.ZonedDateTime,
) -> Entry {
let entry = make_entry(name, EntryType::Link, time?)
entry.target = target
entry
}
///|
/// The transfer type of the data connection, matching the FTP `TYPE` command
/// arguments.
pub(all) enum TransferType {
/// `TYPE I`, binary transfer (also called image type).
Binary
/// `TYPE A`, ASCII transfer.
ASCII
} derive(Eq, @debug.Debug)
///|
/// The argument string sent with the `TYPE` command.
pub fn TransferType::to_string(self : TransferType) -> String {
match self {
Binary => "I"
ASCII => "A"
}
}