basic-ftp
Advanced tools
Comparing version 4.0.2 to 4.1.0
# Changelog | ||
## 4.1.0 | ||
- Added: Support symbolic links in MLSD listings. | ||
## 4.0.2 | ||
@@ -4,0 +8,0 @@ |
@@ -179,3 +179,3 @@ "use strict"; | ||
await this.sendIgnoringError("OPTS UTF8 ON"); // Some servers expect UTF-8 to be enabled explicitly | ||
await this.sendIgnoringError("OPTS MLST type;size;modify;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;"); // Make sure MLSD listings include all we can parse | ||
await this.sendIgnoringError("OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;"); // Make sure MLSD listings include all we can parse | ||
if (this.ftp.hasTLS) { | ||
@@ -182,0 +182,0 @@ await this.sendIgnoringError("PBSZ 0"); // Set to 0 for TLS |
@@ -13,4 +13,3 @@ export declare enum FileType { | ||
/** | ||
* Describes a file, directory or symbolic link. Note that FTP file listings are not standardized. It depends | ||
* on the operating system of the FTP server how complete the information is. | ||
* Describes a file, directory or symbolic link. | ||
*/ | ||
@@ -27,20 +26,47 @@ export declare class FileInfo { | ||
/** | ||
* Unix permissions if present. If the underlying FTP server is not running on Unix or doesn't report | ||
* permissions this will be undefined. If set, you might be able to edit permissions with the FTP command `SITE CHMOD`. | ||
* Unparsed, raw modification date as a string. | ||
* | ||
* If `modifiedAt` is undefined, the FTP server you're connected to doesn't support the more modern | ||
* MLSD command for machine-readable directory listings. The older command LIST is then used returning | ||
* results that vary a lot between servers as the format hasn't been standardized. Here, directory listings | ||
* and especially modification dates were meant to be human-readable first. | ||
* | ||
* Be careful when still trying to parse this by yourself. Parsing dates from listings using LIST is | ||
* unreliable. This library decides to offer parsed dates only when they're absolutely reliable and safe to | ||
* use e.g. for comparisons. | ||
*/ | ||
permissions: UnixPermissions | undefined; | ||
hardLinkCount: number; | ||
link: string; | ||
group: string; | ||
user: string; | ||
rawModifiedAt: string; | ||
/** | ||
* Unparsed date as a string. Be careful when trying to parse this by yourself. There is no | ||
* standard format on which FTP servers agree when using the LIST command. Date information is meant | ||
* to be human-readable but not necessarily easy to parse. See `modifiedAt` for a parsed date. | ||
* Parsed modification date. | ||
* | ||
* Available if the FTP server supports the MLSD command. Only MLSD guarantees dates than can be reliably | ||
* parsed with the correct timezone and a resolution down to seconds. See `rawModifiedAt` property for the unparsed | ||
* date that is always available. | ||
*/ | ||
date: string; | ||
modifiedAt?: Date; | ||
/** | ||
* Parsed modification date is available (and reliable) if the MLSD command is supported by the FTP server. | ||
* Unix permissions if present. If the underlying FTP server is not running on Unix this will be undefined. | ||
* If set, you might be able to edit permissions with the FTP command `SITE CHMOD`. | ||
*/ | ||
modifiedAt: Date | undefined; | ||
permissions?: UnixPermissions; | ||
/** | ||
* Hard link count if available. | ||
*/ | ||
hardLinkCount?: number; | ||
/** | ||
* Link name for symbolic links if available. | ||
*/ | ||
link?: string; | ||
/** | ||
* Unix group if available. | ||
*/ | ||
group?: string; | ||
/** | ||
* Unix user if available. | ||
*/ | ||
user?: string; | ||
/** | ||
* Unique ID if available. | ||
*/ | ||
uniqueID?: string; | ||
constructor(name: string); | ||
@@ -50,2 +76,7 @@ readonly isDirectory: boolean; | ||
readonly isFile: boolean; | ||
/** | ||
* Deprecated, legacy API. Use `rawModifiedAt` instead. | ||
* @deprecated | ||
*/ | ||
date: string; | ||
} |
@@ -11,4 +11,3 @@ "use strict"; | ||
/** | ||
* Describes a file, directory or symbolic link. Note that FTP file listings are not standardized. It depends | ||
* on the operating system of the FTP server how complete the information is. | ||
* Describes a file, directory or symbolic link. | ||
*/ | ||
@@ -20,12 +19,48 @@ class FileInfo { | ||
this.size = 0; | ||
this.hardLinkCount = 0; | ||
this.link = ""; | ||
this.group = ""; | ||
this.user = ""; | ||
/** | ||
* Unparsed date as a string. Be careful when trying to parse this by yourself. There is no | ||
* standard format on which FTP servers agree when using the LIST command. Date information is meant | ||
* to be human-readable but not necessarily easy to parse. See `modifiedAt` for a parsed date. | ||
* Unparsed, raw modification date as a string. | ||
* | ||
* If `modifiedAt` is undefined, the FTP server you're connected to doesn't support the more modern | ||
* MLSD command for machine-readable directory listings. The older command LIST is then used returning | ||
* results that vary a lot between servers as the format hasn't been standardized. Here, directory listings | ||
* and especially modification dates were meant to be human-readable first. | ||
* | ||
* Be careful when still trying to parse this by yourself. Parsing dates from listings using LIST is | ||
* unreliable. This library decides to offer parsed dates only when they're absolutely reliable and safe to | ||
* use e.g. for comparisons. | ||
*/ | ||
this.date = ""; | ||
this.rawModifiedAt = ""; | ||
/** | ||
* Parsed modification date. | ||
* | ||
* Available if the FTP server supports the MLSD command. Only MLSD guarantees dates than can be reliably | ||
* parsed with the correct timezone and a resolution down to seconds. See `rawModifiedAt` property for the unparsed | ||
* date that is always available. | ||
*/ | ||
this.modifiedAt = undefined; | ||
/** | ||
* Unix permissions if present. If the underlying FTP server is not running on Unix this will be undefined. | ||
* If set, you might be able to edit permissions with the FTP command `SITE CHMOD`. | ||
*/ | ||
this.permissions = undefined; | ||
/** | ||
* Hard link count if available. | ||
*/ | ||
this.hardLinkCount = undefined; | ||
/** | ||
* Link name for symbolic links if available. | ||
*/ | ||
this.link = undefined; | ||
/** | ||
* Unix group if available. | ||
*/ | ||
this.group = undefined; | ||
/** | ||
* Unix user if available. | ||
*/ | ||
this.user = undefined; | ||
/** | ||
* Unique ID if available. | ||
*/ | ||
this.uniqueID = undefined; | ||
this.name = name; | ||
@@ -42,2 +77,12 @@ } | ||
} | ||
/** | ||
* Deprecated, legacy API. Use `rawModifiedAt` instead. | ||
* @deprecated | ||
*/ | ||
get date() { | ||
return this.rawModifiedAt; | ||
} | ||
set date(rawModifiedAt) { | ||
this.rawModifiedAt = rawModifiedAt; | ||
} | ||
} | ||
@@ -44,0 +89,0 @@ exports.FileInfo = FileInfo; |
@@ -22,2 +22,9 @@ "use strict"; | ||
]; | ||
function firstCompatibleParser(line, parsers) { | ||
return parsers.find(parser => parser.testLine(line) === true); | ||
} | ||
function stringIsNotBlank(str) { | ||
return str.trim() !== ""; | ||
} | ||
const REGEX_NEWLINE = /\r?\n/; | ||
/** | ||
@@ -27,23 +34,18 @@ * Parse raw directory listing. | ||
function parseList(rawList) { | ||
const lines = rawList.split(/\r?\n/) // Split by newline | ||
.map(line => (/^(\d\d\d)-/.test(line)) ? line.substr(3) : line) // Strip possible multiline prefix | ||
.filter(line => line.trim() !== ""); // Remove blank lines | ||
const lines = rawList | ||
.split(REGEX_NEWLINE) | ||
.filter(stringIsNotBlank); | ||
if (lines.length === 0) { | ||
return []; | ||
} | ||
// Pick the last line of the list as a test candidate to find a compatible parser. | ||
const test = lines[lines.length - 1]; | ||
const parser = firstCompatibleParser(test, availableParsers); | ||
const testLine = lines[lines.length - 1]; | ||
const parser = firstCompatibleParser(testLine, availableParsers); | ||
if (!parser) { | ||
throw new Error("This library only supports MLSD, Unix- or DOS-style directory listing. Your FTP server seems to be using another format. You can see the transmitted listing when setting `client.ftp.verbose = true`. You can then provide a custom parser to `client.parseList`, see the documentation for details."); | ||
} | ||
return lines.map(parser.parseLine) | ||
const files = lines | ||
.map(parser.parseLine) | ||
.filter((info) => info !== undefined); | ||
return parser.transformList(files); | ||
} | ||
exports.parseList = parseList; | ||
/** | ||
* Returns the first parser that doesn't return undefined for the given line. | ||
*/ | ||
function firstCompatibleParser(line, parsers) { | ||
return parsers.find(parser => parser.testLine(line) === true); | ||
} |
import { FileInfo } from "./FileInfo"; | ||
export declare function testLine(line: string): boolean; | ||
export declare function parseLine(line: string): FileInfo | undefined; | ||
export declare function transformList(files: FileInfo[]): FileInfo[]; |
@@ -8,3 +8,3 @@ "use strict"; | ||
* | ||
* http://svn.apache.org/viewvc/commons/proper/net/tags/NET_3_6/src/main/java/org/apache/commons/net/ftp/parser/NTFTPEntryParser.java?revision=1783048&view=markup | ||
* https://github.com/apache/commons-net/blob/master/src/main/java/org/apache/commons/net/ftp/parser/NTFTPEntryParser.java | ||
*/ | ||
@@ -37,3 +37,3 @@ const RE_LINE = new RegExp("(\\S+)\\s+(\\S+)\\s+" // MM-dd-yy whitespace hh:mma|kk:mm swallow trailing spaces | ||
} | ||
file.date = groups[1] + " " + groups[2]; | ||
file.rawModifiedAt = groups[1] + " " + groups[2]; | ||
return file; | ||
@@ -44,1 +44,5 @@ } | ||
exports.parseLine = parseLine; | ||
function transformList(files) { | ||
return files; | ||
} | ||
exports.transformList = transformList; |
@@ -12,2 +12,3 @@ import { FileInfo } from "./FileInfo"; | ||
export declare function parseLine(line: string): FileInfo | undefined; | ||
export declare function transformList(files: FileInfo[]): FileInfo[]; | ||
/** | ||
@@ -14,0 +15,0 @@ * Parse date as specified in https://tools.ietf.org/html/rfc3659#section-2.3. |
@@ -11,3 +11,3 @@ "use strict"; | ||
// - " filename only" | ||
return /\S+=\S+;/.test(line) || line.startsWith(" "); | ||
return /^\S+=\S+;/.test(line) || line.startsWith(" "); | ||
} | ||
@@ -20,9 +20,27 @@ exports.testLine = testLine; | ||
"size": parseSize, | ||
"sized": parseSize, | ||
"sizd": parseSize, | ||
"unique": (value, info) => { | ||
info.uniqueID = value; | ||
}, | ||
"modify": (value, info) => { | ||
info.modifiedAt = parseMLSxDate(value); | ||
info.date = info.modifiedAt.toISOString(); | ||
info.rawModifiedAt = info.modifiedAt.toISOString(); | ||
}, | ||
"type": (value, info) => { | ||
// There seems to be confusion on how to handle symbolic links for Unix. RFC 3659 doesn't describe | ||
// this but mentions some examples using the syntax `type=OS.unix=slink:<target>`. But according to | ||
// an entry in the Errata (https://www.rfc-editor.org/errata/eid1500) this syntax can't be valid. | ||
// Instead it proposes to use `type=OS.unix=symlink` and to then list the actual target of the | ||
// symbolic link as another entry in the directory listing. The unique identifiers can then be used | ||
// to derive the connection between link(s) and target. We'll have to handle both cases as there | ||
// are differing opinions on how to deal with this. Here are some links on this topic: | ||
// - ProFTPD source: https://github.com/proftpd/proftpd/blob/56e6dfa598cbd4ef5c6cba439bcbcd53a63e3b21/modules/mod_facts.c#L531 | ||
// - ProFTPD bug: http://bugs.proftpd.org/show_bug.cgi?id=3318 | ||
// - ProFTPD statement: http://www.proftpd.org/docs/modules/mod_facts.html | ||
// – FileZilla bug: https://trac.filezilla-project.org/ticket/9310 | ||
if (value.startsWith("OS.unix=slink")) { | ||
info.type = FileInfo_1.FileType.SymbolicLink; | ||
info.link = value.substr(value.indexOf(":") + 1); | ||
return false; | ||
} | ||
switch (value) { | ||
@@ -35,2 +53,7 @@ case "file": | ||
break; | ||
case "OS.unix=symlink": | ||
info.type = FileInfo_1.FileType.SymbolicLink; | ||
// The target of the symbolic link might be defined in another line in the directory listing. | ||
// We'll handle this in `transformList()` below. | ||
break; | ||
case "cdir": // Current directory being listed | ||
@@ -56,8 +79,7 @@ case "pdir": // Parent directory | ||
"unix.owner": (value, info) => { | ||
if (info.user === "") | ||
if (info.user === undefined) | ||
info.user = value; | ||
}, | ||
"unix.uid": (value, info) => { | ||
if (info.user === "") | ||
info.user = value; | ||
get "unix.uid"() { | ||
return this["unix.owner"]; | ||
}, | ||
@@ -68,8 +90,7 @@ "unix.groupname": (value, info) => { | ||
"unix.group": (value, info) => { | ||
if (info.group === "") | ||
if (info.group === undefined) | ||
info.group = value; | ||
}, | ||
"unix.gid": (value, info) => { | ||
if (info.group === "") | ||
info.group = value; | ||
get "unix.gid"() { | ||
return this["unix.group"]; | ||
} | ||
@@ -104,3 +125,5 @@ // Regarding the fact "perm": | ||
for (const fact of facts) { | ||
const [factName, factValue] = fact.split("=", 2); | ||
const firstEqualSignPos = fact.indexOf("="); // Consider `type=OS.unix=slink:<target>` | ||
const factName = fact.substr(0, firstEqualSignPos); | ||
const factValue = fact.substr(firstEqualSignPos + 1); | ||
if (!factValue) { | ||
@@ -113,3 +136,3 @@ continue; | ||
} | ||
const shouldIgnoreEntry = factHandler(factValue.toLowerCase(), info); | ||
const shouldIgnoreEntry = factHandler(factValue, info); | ||
if (shouldIgnoreEntry === true) { | ||
@@ -122,2 +145,36 @@ return undefined; | ||
exports.parseLine = parseLine; | ||
function transformList(files) { | ||
// Resolve symbolic links encoded as `type=OS.unix=symlink`. The corresponding target might be | ||
// somewhere in the list. We can identify it using the unique identifier. | ||
const unresolvedSymLinks = []; | ||
for (const file of files) { | ||
if (file.type === FileInfo_1.FileType.SymbolicLink && file.link === undefined && file.uniqueID !== undefined) { | ||
unresolvedSymLinks.push(file); | ||
} | ||
} | ||
if (unresolvedSymLinks.length === 0) { | ||
return files; | ||
} | ||
const resolvedFiles = []; | ||
for (const file of files) { | ||
// It's possible that multiple symbolic links point to the same target. | ||
// We can't resolve anything without unique identifiers. | ||
if (file.type !== FileInfo_1.FileType.SymbolicLink && file.uniqueID !== undefined) { | ||
for (const symLink of unresolvedSymLinks) { | ||
if (symLink.uniqueID === file.uniqueID) { | ||
symLink.link = file.name; | ||
} | ||
} | ||
} | ||
// The targets of a symbolic link is listed as a file in the directory listing but might | ||
// have a path pointing outside of this directory. In that case we don't want this entry | ||
// to be part of the listing. We don't want these kind of entries in general. | ||
const isDirectoryFile = !file.name.includes("/"); | ||
if (isDirectoryFile) { | ||
resolvedFiles.push(file); | ||
} | ||
} | ||
return resolvedFiles; | ||
} | ||
exports.transformList = transformList; | ||
/** | ||
@@ -124,0 +181,0 @@ * Parse date as specified in https://tools.ietf.org/html/rfc3659#section-2.3. |
import { FileInfo } from "./FileInfo"; | ||
export declare function testLine(line: string): boolean; | ||
export declare function parseLine(line: string): FileInfo | undefined; | ||
export declare function transformList(files: FileInfo[]): FileInfo[]; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const FileInfo_1 = require("./FileInfo"); | ||
const JA_MONTH = "\u6708"; | ||
const JA_DAY = "\u65e5"; | ||
const JA_YEAR = "\u5e74"; | ||
/** | ||
@@ -8,3 +11,3 @@ * This parser is based on the FTP client library source code in Apache Commons Net provided | ||
* | ||
* http://svn.apache.org/viewvc/commons/proper/net/tags/NET_3_6/src/main/java/org/apache/commons/net/ftp/parser/ | ||
* https://github.com/apache/commons-net/blob/master/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java | ||
* | ||
@@ -44,10 +47,12 @@ * Below is the regular expression used by this parser. | ||
+ "\\s+" // separator | ||
/* | ||
* numeric or standard format date: | ||
* yyyy-mm-dd (expecting hh:mm to follow) | ||
* MMM [d]d | ||
* [d]d MMM | ||
* N.B. use non-space for MMM to allow for languages such as German which use | ||
* diacritics (e.g. umlaut) in some abbreviations. | ||
*/ | ||
/** | ||
* numeric or standard format date: | ||
* yyyy-mm-dd (expecting hh:mm to follow) | ||
* MMM [d]d | ||
* [d]d MMM | ||
* N.B. use non-space for MMM to allow for languages such as German which use | ||
* diacritics (e.g. umlaut) in some abbreviations. | ||
* Japanese uses numeric day and month with suffixes to distinguish them | ||
* [d]dXX [d]dZZ | ||
*/ | ||
+ "(" + | ||
@@ -57,9 +62,11 @@ "(?:\\d+[-/]\\d+[-/]\\d+)" + // yyyy-mm-dd | ||
"|(?:\\d{1,2}\\s+\\S{3})" + // [d]d MMM | ||
"|(?:\\d{1,2}" + JA_MONTH + "\\s+\\d{1,2}" + JA_DAY + ")" + | ||
")" | ||
+ "\\s+" // separator | ||
/* | ||
year (for non-recent standard format) - yyyy | ||
or time (for numeric or recent standard format) [h]h:mm | ||
*/ | ||
+ "((?:\\d+(?::\\d+)?))" // (20) | ||
/** | ||
* year (for non-recent standard format) - yyyy | ||
* or time (for numeric or recent standard format) [h]h:mm | ||
* or Japanese year - yyyyXX | ||
*/ | ||
+ "((?:\\d+(?::\\d+)?)|(?:\\d{4}" + JA_YEAR + "))" // (20) | ||
+ "\\s" // separator | ||
@@ -85,3 +92,3 @@ + "(.*)"); // the rest (21) | ||
file.hardLinkCount = parseInt(groups[15], 10); | ||
file.date = groups[19] + " " + groups[20]; | ||
file.rawModifiedAt = groups[19] + " " + groups[20]; | ||
file.permissions = { | ||
@@ -129,2 +136,6 @@ user: parseMode(groups[4], groups[5], groups[6]), | ||
exports.parseLine = parseLine; | ||
function transformList(files) { | ||
return files; | ||
} | ||
exports.transformList = transformList; | ||
function parseMode(r, w, x) { | ||
@@ -131,0 +142,0 @@ let value = 0; |
{ | ||
"name": "basic-ftp", | ||
"version": "4.0.2", | ||
"version": "4.1.0", | ||
"description": "FTP client for Node.js, supports explicit FTPS over TLS, IPv6, Async/Await, and Typescript.", | ||
@@ -39,8 +39,8 @@ "main": "dist/index", | ||
"devDependencies": { | ||
"@types/node": "12.7.8", | ||
"@typescript-eslint/eslint-plugin": "2.3.1", | ||
"@typescript-eslint/parser": "2.3.1", | ||
"eslint": "6.4.0", | ||
"@types/node": "12.7.12", | ||
"@typescript-eslint/eslint-plugin": "2.3.3", | ||
"@typescript-eslint/parser": "2.3.3", | ||
"eslint": "6.5.1", | ||
"js-yaml": ">=3.13.1", | ||
"mocha": "6.2.0", | ||
"mocha": "6.2.1", | ||
"rimraf": "3.0.0", | ||
@@ -47,0 +47,0 @@ "typescript": "3.6.3" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
131263
2615