first commit
This commit is contained in:
55
build/node_modules/basic-ftp/lib/FileInfo.js
generated
vendored
Normal file
55
build/node_modules/basic-ftp/lib/FileInfo.js
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Holds information about a remote file.
|
||||
*/
|
||||
module.exports = class FileInfo {
|
||||
|
||||
static get Type() {
|
||||
return {
|
||||
File: 0,
|
||||
Directory: 1,
|
||||
SymbolicLink: 2,
|
||||
Unknown: 3
|
||||
};
|
||||
}
|
||||
|
||||
static get Permission() {
|
||||
return {
|
||||
Read: 1,
|
||||
Write: 2,
|
||||
Execute: 4
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.type = FileInfo.Type.Unknown;
|
||||
this.size = -1;
|
||||
this.hardLinkCount = 0;
|
||||
this.permissions = {
|
||||
user: 0,
|
||||
group: 0,
|
||||
world: 0
|
||||
};
|
||||
this.link = "";
|
||||
this.group = "";
|
||||
this.user = "";
|
||||
this.date = "";
|
||||
}
|
||||
|
||||
get isFile() {
|
||||
return this.type === FileInfo.Type.File;
|
||||
}
|
||||
|
||||
get isDirectory() {
|
||||
return this.type === FileInfo.Type.Directory;
|
||||
}
|
||||
|
||||
get isSymbolicLink() {
|
||||
return this.type === FileInfo.Type.SymbolicLink;
|
||||
}
|
||||
};
|
||||
447
build/node_modules/basic-ftp/lib/ftp.js
generated
vendored
Normal file
447
build/node_modules/basic-ftp/lib/ftp.js
generated
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
"use strict";
|
||||
|
||||
const Socket = require("net").Socket;
|
||||
const tls = require("tls");
|
||||
const parseListUnix = require("./parseListUnix");
|
||||
|
||||
/**
|
||||
* Minimal requirements for an FTP client.
|
||||
*/
|
||||
class Client {
|
||||
|
||||
/**
|
||||
* Create a client instance.
|
||||
* @param {number} timeoutMillis Timeout to apply to control and data connections. Use 0 for no timeout.
|
||||
*/
|
||||
constructor(timeoutMillis = 0) {
|
||||
// A timeout can be applied to the control connection.
|
||||
this._timeoutMillis = timeoutMillis;
|
||||
// The current task to be resolved or rejected.
|
||||
this._task = undefined;
|
||||
// A function that handles incoming messages and resolves or rejects a task.
|
||||
this._handler = undefined;
|
||||
// Options for TLS connections.
|
||||
this.tlsOptions = {};
|
||||
// The client can log every outgoing and incoming message.
|
||||
this.verbose = false;
|
||||
// The control connection to the FTP server.
|
||||
this.socket = new Socket();
|
||||
// The data connection to the FTP server.
|
||||
this.dataSocket = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes control and data sockets.
|
||||
*/
|
||||
close() {
|
||||
this.log("Closing sockets.");
|
||||
this._closeSocket(this._socket);
|
||||
this._closeSocket(this._dataSocket);
|
||||
}
|
||||
|
||||
get socket() {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
set socket(socket) {
|
||||
if (this._socket) {
|
||||
// Don't destroy the existing control socket automatically.
|
||||
// The setter might have been called to upgrade an existing connection.
|
||||
this._socket.removeAllListeners();
|
||||
}
|
||||
this._socket = this._setupSocket(socket);
|
||||
if (this._socket) {
|
||||
this._socket.setKeepAlive(true);
|
||||
this._socket.on("data", data => {
|
||||
const message = data.toString().trim();
|
||||
const code = parseInt(message.substr(0, 3), 10);
|
||||
this.log(`< ${message}`);
|
||||
this._respond({ code, message });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get dataSocket() {
|
||||
return this._dataSocket;
|
||||
}
|
||||
|
||||
set dataSocket(socket) {
|
||||
if (this._dataSocket) {
|
||||
this._dataSocket.destroy();
|
||||
this._dataSocket.removeAllListeners();
|
||||
}
|
||||
this._dataSocket = this._setupSocket(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if TLS is enabled for the control socket.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasTLS() {
|
||||
return this._socket && this._socket.getSession !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an FTP command.
|
||||
* @param {string} command
|
||||
*/
|
||||
send(command) {
|
||||
const hasPass = command.indexOf("PASS") !== -1;
|
||||
const message = hasPass ? "> PASS ###" : `> ${command}`;
|
||||
this.log(message);
|
||||
this._socket.write(command + "\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an FTP command and handle any response until the newly task is resolved.
|
||||
* @param {string} command
|
||||
* @param {HandlerCallback} handler
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
handle(command, handler) {
|
||||
return new Promise((resolvePromise, rejectPromise) => {
|
||||
this._handler = handler;
|
||||
this._task = {
|
||||
// When resolving or rejecting we also want the handler
|
||||
// to no longer receive any responses or errors.
|
||||
resolve: (...args) => {
|
||||
this._handler = undefined;
|
||||
resolvePromise(...args);
|
||||
},
|
||||
reject: (...args) => {
|
||||
this._handler = undefined;
|
||||
rejectPromise(...args);
|
||||
}
|
||||
};
|
||||
if (command !== undefined) {
|
||||
this.send(command);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs message if client is verbose
|
||||
* @param {string} message
|
||||
*/
|
||||
log(message) {
|
||||
if (this.verbose) {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
_respond(payload) {
|
||||
if (this._handler) {
|
||||
this._handler(payload, this._task);
|
||||
}
|
||||
}
|
||||
|
||||
_setupSocket(socket) {
|
||||
if (socket) {
|
||||
// All sockets share the same timeout.
|
||||
socket.setTimeout(this._timeoutMillis);
|
||||
// Reroute any events to the single communication channel with the currently responsible handler.
|
||||
socket.on("error", error => this._respond({ error }));
|
||||
socket.on("timeout", () => this._respond({ error: "Timeout" }));
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
_closeSocket(socket) {
|
||||
if (socket) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
module.exports = {
|
||||
// Basic API
|
||||
Client,
|
||||
connect,
|
||||
send,
|
||||
useTLS,
|
||||
enterPassiveMode,
|
||||
list,
|
||||
download,
|
||||
upload,
|
||||
// Convenience
|
||||
login,
|
||||
useDefaultSettings,
|
||||
// Useful for custom extensions
|
||||
parseIPV4PasvResponse,
|
||||
upgradeSocket
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to an FTP server.
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {string} host
|
||||
* @param {number} port
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function connect(client, host, port) {
|
||||
client.socket.connect(port, host);
|
||||
return client.handle(undefined, (res, task) => {
|
||||
if (res.code === 220) {
|
||||
task.resolve();
|
||||
}
|
||||
else {
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an FTP command.
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {string} command
|
||||
* @param {boolean} ignoreError
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
function send(client, command, ignoreErrorCodes = false) {
|
||||
return client.handle(command, (res, task) => {
|
||||
const success = res.code >= 200 && res.code < 400;
|
||||
if (success || (res.code && ignoreErrorCodes)) {
|
||||
task.resolve(res.code);
|
||||
}
|
||||
else {
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the current socket connection to TLS.
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {Object} options
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function useTLS(client, options) {
|
||||
return client.handle("AUTH TLS", (res, task) => {
|
||||
if (res.code === 200 || res.code === 234) {
|
||||
upgradeSocket(client.socket, options).then(tlsSocket => {
|
||||
client.log("Control socket is using " + tlsSocket.getProtocol());
|
||||
client.socket = tlsSocket; // TLS socket is the control socket from now on
|
||||
client.tlsOptions = options; // Keep the TLS options for later data connections that should use the same options.
|
||||
task.resolve();
|
||||
}).catch(err => task.reject(err));
|
||||
}
|
||||
else {
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade a socket connection.
|
||||
*
|
||||
* @param {Socket} socket
|
||||
* @param {Object} options The options for tls.connect(options)
|
||||
*/
|
||||
function upgradeSocket(socket, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
options = Object.assign({}, options, {
|
||||
socket // Establish the secure connection using the existing socket connection.
|
||||
});
|
||||
const tlsSocket = tls.connect(options, () => {
|
||||
const expectCertificate = options.rejectUnauthorized !== false;
|
||||
if (expectCertificate && !tlsSocket.authorized) {
|
||||
reject(tlsSocket.authorizationError);
|
||||
}
|
||||
else {
|
||||
resolve(tlsSocket);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a data socket using passive mode.
|
||||
*
|
||||
* @param {Client} client
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function enterPassiveMode(client, parsePasvResponse = parseIPV4PasvResponse) {
|
||||
return client.handle("PASV", (res, task) => {
|
||||
if (res.code === 227) {
|
||||
const target = parsePasvResponse(res.message);
|
||||
if (!target) {
|
||||
task.reject("Can't parse PASV response", res.message);
|
||||
return;
|
||||
}
|
||||
let socket = new Socket();
|
||||
socket.once("error", err => {
|
||||
task.reject("Can't open data connection in passive mode: " + err.message);
|
||||
});
|
||||
socket.connect(target.port, target.host, () => {
|
||||
if (client.hasTLS) {
|
||||
// Upgrade to TLS, reuse TLS session of control socket.
|
||||
const options = Object.assign({}, client.tlsOptions, {
|
||||
socket,
|
||||
session: client.socket.getSession()
|
||||
});
|
||||
socket = tls.connect(options);
|
||||
client.log("Data socket uses " + socket.getProtocol());
|
||||
}
|
||||
socket.removeAllListeners();
|
||||
client.dataSocket = socket;
|
||||
task.resolve();
|
||||
});
|
||||
}
|
||||
else {
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const regexPasv = /([-\d]+,[-\d]+,[-\d]+,[-\d]+),([-\d]+),([-\d]+)/;
|
||||
|
||||
/**
|
||||
* Parse a PASV response message.
|
||||
*
|
||||
* @param {string} message
|
||||
* @returns {{host: string, port: number}}
|
||||
*/
|
||||
function parseIPV4PasvResponse(message) {
|
||||
// From something like "227 Entering Passive Mode (192,168,3,200,10,229)",
|
||||
// extract host and port.
|
||||
const groups = message.match(regexPasv);
|
||||
if (!groups || groups.length !== 4) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
host: groups[1].replace(/,/g, "."),
|
||||
port: (parseInt(groups[2], 10) & 255) * 256 + (parseInt(groups[3], 10) & 255)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List files and folders of current directory.`
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {(rawList: string) => FileInfo[]} parseList
|
||||
* @return {FileInfo[]>}
|
||||
*/
|
||||
function list(client, parseList = parseListUnix) {
|
||||
// Some FTP servers transmit the list data and then confirm on the
|
||||
// control socket that the transfer is complete, others do it the
|
||||
// other way around. We'll need to make sure that we wait until
|
||||
// both the data and the confirmation have arrived.
|
||||
let ctrlDone = false;
|
||||
let rawList = "";
|
||||
let parsedList = undefined;
|
||||
return client.handle("LIST", (res, task) => {
|
||||
if (res.code === 150) { // Ready to download
|
||||
client.dataSocket.on("data", data => {
|
||||
rawList += data.toString();
|
||||
});
|
||||
client.dataSocket.once("end", () => {
|
||||
client.dataSocket = undefined;
|
||||
client.log(rawList);
|
||||
parsedList = parseList(rawList);
|
||||
if (ctrlDone) {
|
||||
task.resolve(parsedList);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (res.code === 226) { // Transfer complete
|
||||
ctrlDone = true;
|
||||
if (parsedList) {
|
||||
task.resolve(parsedList);
|
||||
}
|
||||
}
|
||||
else if (res.code > 400 || res.error) {
|
||||
client.dataSocket = undefined;
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload stream data as a file. For example:
|
||||
*
|
||||
* `upload(client, fs.createReadStream(localFilePath), remoteFilename)`
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {stream.Readable} readableStream
|
||||
* @param {string} remoteFilename
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function upload(client, readableStream, remoteFilename) {
|
||||
const command = "STOR " + remoteFilename;
|
||||
return client.handle(command, (res, task) => {
|
||||
if (res.code === 150) { // Ready to upload
|
||||
// If all data has been flushed, close the socket to signal
|
||||
// the FTP server that the transfer is complete.
|
||||
client.dataSocket.on("finish", () => client.dataSocket = undefined);
|
||||
readableStream.pipe(client.dataSocket);
|
||||
}
|
||||
else if (res.code === 226) { // Transfer complete
|
||||
task.resolve();
|
||||
}
|
||||
else if (res.code > 400 || res.error) {
|
||||
client.dataSocket = undefined;
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file as a stream. For example:
|
||||
*
|
||||
* `download(client, fs.createWriteStream(localFilePath), remoteFilename)`
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {stream.Writable} writableStream
|
||||
* @param {string} remoteFilename
|
||||
* @param {number} startAt
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function download(client, writableStream, remoteFilename, startAt = 0) {
|
||||
const command = startAt > 0 ? `REST ${startAt}` : `RETR ${remoteFilename}`;
|
||||
return client.handle(command, (res, task) => {
|
||||
if (res.code === 150) { // Ready to download
|
||||
client.dataSocket.pipe(writableStream);
|
||||
}
|
||||
else if (res.code === 350) { // Restarting at startAt.
|
||||
client.send("RETR " + remoteFilename);
|
||||
}
|
||||
else if (res.code === 226) { // Transfer complete
|
||||
client.dataSocket = undefined;
|
||||
task.resolve();
|
||||
}
|
||||
else if (res.code > 400 || res.error) {
|
||||
client.dataSocket = undefined;
|
||||
task.reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
*/
|
||||
async function login(client, user, password) {
|
||||
await send(client, "USER " + user);
|
||||
await send(client, "PASS " + password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set some default settings.
|
||||
*
|
||||
* @param {Client} client
|
||||
*/
|
||||
async function useDefaultSettings(client) {
|
||||
await send(client, "TYPE I"); // Binary mode
|
||||
await send(client, "STRU F"); // Use file structure
|
||||
if (client.hasTLS) {
|
||||
await send(client, "PBSZ 0", true); // Set to 0 for TLS
|
||||
await send(client, "PROT P", true); // Protect channel (also for data connections)
|
||||
}
|
||||
}
|
||||
164
build/node_modules/basic-ftp/lib/parseListUnix.js
generated
vendored
Normal file
164
build/node_modules/basic-ftp/lib/parseListUnix.js
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This parser is based on the FTP client library source code in Apache Commons Net provided
|
||||
* under the Apache 2.0 license. It has been simplified and rewritten to better fit the Javascript language.
|
||||
*
|
||||
* @see http://svn.apache.org/viewvc/commons/proper/net/tags/NET_3_6/src/main/java/org/apache/commons/net/ftp/parser/
|
||||
*/
|
||||
|
||||
const FileInfo = require("./FileInfo");
|
||||
|
||||
/**
|
||||
* Parses raw list data as a Unix listing.
|
||||
* @param {string} rawList
|
||||
* @returns {FileInfo[]}
|
||||
*/
|
||||
module.exports = function(rawList) {
|
||||
return rawList.split(/\r?\n/)
|
||||
// Strip possible multiline prefix
|
||||
.map(line => (/^(\d\d\d)-/.test(line)) ? line.substr(3) : line)
|
||||
.map(parseLine)
|
||||
.filter(fileRef => fileRef !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the regular expression used by this parser.
|
||||
*
|
||||
* Permissions:
|
||||
* r the file is readable
|
||||
* w the file is writable
|
||||
* x the file is executable
|
||||
* - the indicated permission is not granted
|
||||
* L mandatory locking occurs during access (the set-group-ID bit is
|
||||
* on and the group execution bit is off)
|
||||
* s the set-user-ID or set-group-ID bit is on, and the corresponding
|
||||
* user or group execution bit is also on
|
||||
* S undefined bit-state (the set-user-ID bit is on and the user
|
||||
* execution bit is off)
|
||||
* t the 1000 (octal) bit, or sticky bit, is on [see chmod(1)], and
|
||||
* execution is on
|
||||
* T the 1000 bit is turned on, and execution is off (undefined bit-
|
||||
* state)
|
||||
* e z/OS external link bit
|
||||
* Final letter may be appended:
|
||||
* + file has extended security attributes (e.g. ACL)
|
||||
* Note: local listings on MacOSX also use '@';
|
||||
* this is not allowed for here as does not appear to be shown by FTP servers
|
||||
* {@code @} file has extended attributes
|
||||
*/
|
||||
const RE_LINE = new RegExp(
|
||||
"([bcdelfmpSs-])" // file type
|
||||
+"(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\\+?" // permissions
|
||||
|
||||
+ "\\s*" // separator TODO why allow it to be omitted??
|
||||
|
||||
+ "(\\d+)" // link count
|
||||
|
||||
+ "\\s+" // separator
|
||||
|
||||
+ "(?:(\\S+(?:\\s\\S+)*?)\\s+)?" // owner name (optional spaces)
|
||||
+ "(?:(\\S+(?:\\s\\S+)*)\\s+)?" // group name (optional spaces)
|
||||
+ "(\\d+(?:,\\s*\\d+)?)" // size or n,m
|
||||
|
||||
+ "\\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.
|
||||
*/
|
||||
+ "("+
|
||||
"(?:\\d+[-/]\\d+[-/]\\d+)" + // yyyy-mm-dd
|
||||
"|(?:\\S{3}\\s+\\d{1,2})" + // MMM [d]d
|
||||
"|(?:\\d{1,2}\\s+\\S{3})" + // [d]d MMM
|
||||
")"
|
||||
|
||||
+ "\\s+" // separator
|
||||
|
||||
/*
|
||||
year (for non-recent standard format) - yyyy
|
||||
or time (for numeric or recent standard format) [h]h:mm
|
||||
*/
|
||||
+ "((?:\\d+(?::\\d+)?))" // (20)
|
||||
|
||||
+ "\\s" // separator
|
||||
|
||||
+ "(.*)"); // the rest (21)
|
||||
|
||||
const accessGroups = ["user", "group", "world"];
|
||||
|
||||
function parseLine(line) {
|
||||
const groups = line.match(RE_LINE);
|
||||
if (groups) {
|
||||
// Ignore parent directory links
|
||||
const name = groups[21].trim();
|
||||
if (name === "." || name === "..") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Map list entry to FileInfo instance
|
||||
const file = new FileInfo(name);
|
||||
file.size = parseInt(groups[18], 10);
|
||||
file.user = groups[16];
|
||||
file.group = groups[17];
|
||||
file.hardLinkCount = parseInt(groups[15], 10);
|
||||
file.date = groups[19] + " " + groups[20];
|
||||
|
||||
// Set file type
|
||||
switch (groups[1].charAt(0)) {
|
||||
case "d":
|
||||
file.type = FileInfo.Type.Directory;
|
||||
break;
|
||||
case "e": // NET-39 => z/OS external link
|
||||
file.type = FileInfo.Type.SymbolicLink;
|
||||
break;
|
||||
case "l":
|
||||
file.type = FileInfo.Type.SymbolicLink;
|
||||
break;
|
||||
case "b":
|
||||
case "c":
|
||||
file.type = FileInfo.Type.File; // TODO change this if DEVICE_TYPE implemented
|
||||
break;
|
||||
case "f":
|
||||
case "-":
|
||||
file.type = FileInfo.Type.File;
|
||||
break;
|
||||
default:
|
||||
// A 'whiteout' file is an ARTIFICIAL entry in any of several types of
|
||||
// 'translucent' filesystems, of which a 'union' filesystem is one.
|
||||
file.type = FileInfo.Type.Unknown;
|
||||
}
|
||||
|
||||
// Set permissions
|
||||
accessGroups.forEach((access, i) => {
|
||||
const g = (i + 1) * 4;
|
||||
let value = 0;
|
||||
if (groups[g] !== "-") {
|
||||
value += FileInfo.Permission.Read;
|
||||
}
|
||||
if (groups[g+1] !== "-") {
|
||||
value += FileInfo.Permission.Write;
|
||||
}
|
||||
const execToken = groups[g+2].charAt(0);
|
||||
if (execToken !== "-" && execToken.toUpperCase() !== execToken) {
|
||||
value += FileInfo.Permission.Execute;
|
||||
}
|
||||
file.permissions[access] = value;
|
||||
});
|
||||
|
||||
// Separate out the link name for symbolic links
|
||||
if (file.isSymbolicLink) {
|
||||
const end = name.indexOf(" -> ");
|
||||
if (end > -1) {
|
||||
file.name = name.substring(0, end);
|
||||
file.link = name.substring(end + 4);
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user