mirror of
https://github.com/IgorTimofeev/MineOS.git
synced 2025-12-20 11:09:21 +01:00
FTP rework
This commit is contained in:
parent
283d0fca68
commit
3f34c05a7b
928
Libraries/FTP.lua
Normal file
928
Libraries/FTP.lua
Normal file
@ -0,0 +1,928 @@
|
|||||||
|
local event = require("Event")
|
||||||
|
local filesystem = require("Filesystem")
|
||||||
|
|
||||||
|
local FTP = {}
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Connect to address:port and wait for the socket to finish connection until timeout exceeded
|
||||||
|
local function socketConnect(address, port, timeout)
|
||||||
|
local socket, reason = component.get("internet").connect(address, port)
|
||||||
|
|
||||||
|
local connectionStartTime = computer.uptime()
|
||||||
|
while true do
|
||||||
|
local success, reason = socket.finishConnect()
|
||||||
|
if success then
|
||||||
|
return socket
|
||||||
|
end
|
||||||
|
|
||||||
|
if success == nil then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
if computer.uptime() - connectionStartTime > timeout then
|
||||||
|
return nil, "connection timed out"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Wait for internet_ready event until timeout exceeded
|
||||||
|
local function socketAwait(socket, timeout)
|
||||||
|
local startTime = computer.uptime()
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local eventType, _, socketId = event.pull(timeout - (computer.uptime() - startTime))
|
||||||
|
|
||||||
|
if not eventType then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if eventType == "internet_ready" and socketId == socket.id() then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read remaining data from the socket
|
||||||
|
local function socketReceive(socket)
|
||||||
|
local data = ""
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local chunk = socket.read()
|
||||||
|
|
||||||
|
if not chunk or #chunk == 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
data = data .. chunk
|
||||||
|
end
|
||||||
|
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- FTP response line iterator
|
||||||
|
function FTP.lines(data)
|
||||||
|
return data:gmatch("([^\r\n]+)\r\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Convert FTP modification time into unix timestamp
|
||||||
|
function FTP.parseTimestamp(timestamp)
|
||||||
|
local year, month, day, hour, min, sec = timestamp:match("(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)")
|
||||||
|
|
||||||
|
return os.time({
|
||||||
|
year = tonumber(year),
|
||||||
|
month = tonumber(month),
|
||||||
|
day = tonumber(day),
|
||||||
|
hour = tonumber(hour),
|
||||||
|
min = tonumber(min),
|
||||||
|
sec = tonumber(sec)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- File information returned by MLSD/MLST commands follows key=value; format:
|
||||||
|
-- type=dir;sizd=4096;modify=20240924194606; home
|
||||||
|
function FTP.parseFileInfo(fileInfo)
|
||||||
|
local info = {}
|
||||||
|
|
||||||
|
for token in fileInfo:gmatch("[^; ]+") do
|
||||||
|
local key, value = token:match("(.+)=(.+)")
|
||||||
|
|
||||||
|
if key then
|
||||||
|
key = key:lower()
|
||||||
|
|
||||||
|
if key == "type" then
|
||||||
|
info.isdir = not not value:match("dir")
|
||||||
|
elseif key:match("siz.") then
|
||||||
|
info.size = tonumber(value)
|
||||||
|
elseif key == "modify" then
|
||||||
|
info.modify = FTP.parseTimestamp(value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
info.name = token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return info
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Receive and parse FTP command response
|
||||||
|
-- If the function succeedes, the return values will be first responses and a table containing all parsed responses
|
||||||
|
-- Most of the time second value may be discarded, though it may be useful when sending multiple commands in a single request
|
||||||
|
local function FTPReceiveResponse(self)
|
||||||
|
if not socketAwait(self.controlSocket, self.responseTimeout) then
|
||||||
|
return nil, "response timed out"
|
||||||
|
end
|
||||||
|
|
||||||
|
local data, reason = socketReceive(self.controlSocket)
|
||||||
|
if not data then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local responses, response = {}
|
||||||
|
for line in data:gmatch("[^\r\n]+\r\n") do
|
||||||
|
if not response then
|
||||||
|
response = {
|
||||||
|
content = ""
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local status, delimiter, content = line:match("(%d%d%d)([ -])(.+\r\n)")
|
||||||
|
|
||||||
|
if status then
|
||||||
|
response.content = response.content .. content
|
||||||
|
|
||||||
|
-- Space after the status code indicates the last line of the response
|
||||||
|
if delimiter == " " then
|
||||||
|
response.status = tonumber(status)
|
||||||
|
response.ok = 100 <= response.status and response.status < 400
|
||||||
|
|
||||||
|
table.insert(responses, response)
|
||||||
|
response = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
response.content = response.content .. line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #responses < 1 then
|
||||||
|
return nil, "no parsable responses"
|
||||||
|
end
|
||||||
|
|
||||||
|
return responses[1], responses
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Send FTP command and receive response
|
||||||
|
-- command argument may be either a string or a table containing multiple commands
|
||||||
|
local function FTPSendCommand(self, command)
|
||||||
|
command = (type(command) == "table" and table.concat(command, "\r\n") or command) .. "\r\n"
|
||||||
|
|
||||||
|
-- Seems like windows IIS ftp server doesn't like large command blobs
|
||||||
|
for i = 1, math.ceil(#command / self.commandChunkSize) do
|
||||||
|
local result, reason = self.controlSocket.write(
|
||||||
|
command:sub(
|
||||||
|
self.commandChunkSize * (i - 1) + 1,
|
||||||
|
self.commandChunkSize * i
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return self:receiveResponse()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Enter passive mode, send command and read data connection
|
||||||
|
local function FTPSendPassiveModeCommand(self, command)
|
||||||
|
local result, reason = self:beginNegotiation()
|
||||||
|
if not result then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand(command)
|
||||||
|
if not response then
|
||||||
|
self:endNegotiation()
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
if not response.ok then
|
||||||
|
self:endNegotiation()
|
||||||
|
return nil, "command failed: " .. response.content
|
||||||
|
end
|
||||||
|
|
||||||
|
local data, reason = self:receiveData()
|
||||||
|
self:endNegotiation()
|
||||||
|
|
||||||
|
if not data then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Enter passive mode and establish data connection
|
||||||
|
local function FTPBeginNegotiation(self)
|
||||||
|
if self.dataSocket then
|
||||||
|
return nil, "data connection already open"
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("PASV")
|
||||||
|
if not response then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
if not response.ok then
|
||||||
|
return nil, "unable to enter passive mode: " .. response.content
|
||||||
|
end
|
||||||
|
|
||||||
|
local digits = {response.content:match("(%d+),(%d+),(%d+),(%d+),(%d+),(%d+)")}
|
||||||
|
if #digits ~= 6 then
|
||||||
|
return nil, "unable to enter passive mode: malformed response"
|
||||||
|
end
|
||||||
|
|
||||||
|
local dataSocket, reason = socketConnect(
|
||||||
|
table.concat(digits, ".", 1, 4),
|
||||||
|
tonumber(digits[5]) * 256 + tonumber(digits[6]),
|
||||||
|
self.responseTimeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dataSocket then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
self.dataSocket = dataSocket
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Close data connection
|
||||||
|
local function FTPEndNegotiation(self)
|
||||||
|
if not self.dataSocket then
|
||||||
|
return nil, "data connection was not open"
|
||||||
|
end
|
||||||
|
|
||||||
|
self.dataSocket.close()
|
||||||
|
self.dataSocket = nil
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read data response
|
||||||
|
local function FTPReceiveData(self)
|
||||||
|
if not self.dataSocket then
|
||||||
|
return nil, "data connection was not open"
|
||||||
|
end
|
||||||
|
|
||||||
|
return socketReceive(self.dataSocket)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Send data through the data connection
|
||||||
|
local function FTPSendData(self, data)
|
||||||
|
if not self.dataSocket then
|
||||||
|
return nil, "data connection was not open"
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.dataSocket.write(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Close FTP connection
|
||||||
|
local function FTPClose(self)
|
||||||
|
if self.controlSocket then
|
||||||
|
self.controlSocket.close()
|
||||||
|
self.controlSocket = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.dataSocket then
|
||||||
|
self.dataSocket.close()
|
||||||
|
self.dataSocket = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function pizda(response)
|
||||||
|
return not response or not response.ok
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cyka(response, ...)
|
||||||
|
if not response then
|
||||||
|
return nil, ...
|
||||||
|
end
|
||||||
|
|
||||||
|
if not response.ok then
|
||||||
|
return nil, response.content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getParentDirectory(path)
|
||||||
|
local directory, filename = path:match("^(.*/)([^/]+)/?$")
|
||||||
|
if not directory then
|
||||||
|
return nil, path
|
||||||
|
end
|
||||||
|
|
||||||
|
return directory, filename
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function FTPSetCacheValue(self, key, value)
|
||||||
|
self.cache[key] = value ~= nil and {computer.uptime(), value} or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FTPGetCacheValue(self, key)
|
||||||
|
local currentTime = computer.uptime()
|
||||||
|
for key, entry in pairs(self.cache) do
|
||||||
|
if currentTime - entry[1] > self.cacheTimeout then
|
||||||
|
self.cache[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local entry = self.cache[key]
|
||||||
|
if entry then
|
||||||
|
return entry[2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Keep connection alive
|
||||||
|
local function FTPKeepAlive(self)
|
||||||
|
local response, reason = self:sendCommand("PWD")
|
||||||
|
if not response then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Login
|
||||||
|
local function FTPLogin(self, username, password)
|
||||||
|
local response, reason = self:sendCommand("USER " .. username)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("PASS " .. password)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set negotiation mode
|
||||||
|
local function FTPSetMode(self, mode)
|
||||||
|
local response, reason = self:sendCommand("TYPE " .. mode)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get working directory
|
||||||
|
local function FTPGetWorkingDirectory(self, useCache)
|
||||||
|
if self.cacheTimeout and useCache then
|
||||||
|
local cache = self:getCacheValue("getWorkingDirectory")
|
||||||
|
|
||||||
|
if cache then
|
||||||
|
return cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("PWD")
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
local path = response.content:match("\"(.+)\"")
|
||||||
|
if not path then
|
||||||
|
return nil, "unexpected response: " .. response.content
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.cacheTimeout then
|
||||||
|
self:setCacheValue("getWorkingDirectory", path)
|
||||||
|
end
|
||||||
|
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set working directory
|
||||||
|
local function FTPChangeWorkingDirectory(self, path)
|
||||||
|
if self.cacheTimeout then
|
||||||
|
self:setCacheValue("getWorkingDirectory")
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("CWD " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get file size
|
||||||
|
local function FTPGetFileSize(self, path)
|
||||||
|
local response, reason = self:sendCommand("SIZE " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
return tonumber(response.content)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns absolute path
|
||||||
|
local function FTPResolvePath(self, path, useCache)
|
||||||
|
if path:match("^/.*") then
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
local directory, reason = self:getWorkingDirectory(useCache)
|
||||||
|
if not directory then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
return directory .. "/" .. path
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function is used internally by listDirectory and fileInfo methods
|
||||||
|
local function FTPRequestFilesInformation(self, fileNames)
|
||||||
|
local commands = { "PWD" }
|
||||||
|
|
||||||
|
for _, path in pairs(fileNames) do
|
||||||
|
-- One way to find out whether entry is directory or not without MLSD/MLST is to try to CWD to it
|
||||||
|
table.insert(commands, "CWD " .. path)
|
||||||
|
table.insert(commands, "MDTM " .. path)
|
||||||
|
table.insert(commands, "SIZE " .. path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, responsesOrReason = self:sendCommand(commands)
|
||||||
|
if not success then
|
||||||
|
return nil, responsesOrReason
|
||||||
|
end
|
||||||
|
|
||||||
|
local workingDirectory = table.remove(responsesOrReason, 1).content:match("\"(.+)\"")
|
||||||
|
|
||||||
|
local list = {}
|
||||||
|
for _, path in pairs(fileNames) do
|
||||||
|
local entry = {}
|
||||||
|
entry.name = path:match("/([^/]+)$") or path
|
||||||
|
entry.isdir = table.remove(responsesOrReason, 1).ok
|
||||||
|
|
||||||
|
local response = table.remove(responsesOrReason, 1)
|
||||||
|
entry.modify = response.ok and FTP.parseTimestamp(response.content) or 0
|
||||||
|
|
||||||
|
local response = table.remove(responsesOrReason, 1)
|
||||||
|
entry.size = response.ok and tonumber(response.content) or 0
|
||||||
|
|
||||||
|
table.insert(list, entry)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:changeWorkingDirectory(workingDirectory)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- List directory entries with their type, size and modification time
|
||||||
|
local function FTPListDirectory(self, path, useCache)
|
||||||
|
local reason
|
||||||
|
path, reason = self:resolvePath(path, useCache)
|
||||||
|
if not path then
|
||||||
|
return nil, "unable to resolve path: " .. reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local cacheKey = "listDirectory@" .. path
|
||||||
|
if self.cacheTimeout and useCache then
|
||||||
|
local cache = self:getCacheValue(cacheKey)
|
||||||
|
|
||||||
|
if cache then
|
||||||
|
return cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local list = {}
|
||||||
|
if self.features.MLSD then
|
||||||
|
local data, reason = self:sendPassiveModeCommand("MLSD " .. path)
|
||||||
|
if not data then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
for line in FTP.lines(data) do
|
||||||
|
local info = FTP.parseFileInfo(line)
|
||||||
|
|
||||||
|
if not info.name:match("[^%.]*%.%.?$") then
|
||||||
|
table.insert(list, info)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- No MLSD :(
|
||||||
|
local data, reason = self:sendPassiveModeCommand("NLST " .. path)
|
||||||
|
if not data then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local fileNames = {}
|
||||||
|
for name in FTP.lines(data) do
|
||||||
|
if not name:match("[^%.]*%.%.?$") then
|
||||||
|
table.insert(fileNames, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local reason
|
||||||
|
list, reason = self:requestFilesInformation(fileNames)
|
||||||
|
if not list then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort files by name
|
||||||
|
table.sort(
|
||||||
|
list,
|
||||||
|
function(a, b)
|
||||||
|
return a.name < b.name
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.cacheTimeout then
|
||||||
|
self:setCacheValue(cacheKey, list)
|
||||||
|
end
|
||||||
|
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns information about the requested file
|
||||||
|
local function FTPGetFileInfo(self, path, useCache)
|
||||||
|
local reason
|
||||||
|
path, reason = self:resolvePath(path, useCache)
|
||||||
|
if not path then
|
||||||
|
return nil, "unable to resolve path: " .. reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local cacheKey = "getFileInfo@" .. path
|
||||||
|
if self.cacheTimeout and useCache then
|
||||||
|
-- Check for getFileInfo cache
|
||||||
|
local cache = self:getCacheValue(cacheKey)
|
||||||
|
if cache then
|
||||||
|
return cache
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for listDirectory cache
|
||||||
|
local directory, filename = getParentDirectory(path)
|
||||||
|
if directory then
|
||||||
|
local cache = self:getCacheValue("listDirectory@" .. directory)
|
||||||
|
|
||||||
|
if cache then
|
||||||
|
for i = 1, #cache do
|
||||||
|
if cache[i].name == filename then
|
||||||
|
return cache[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local info
|
||||||
|
if self.features.MLST then
|
||||||
|
local response, reason = self:sendCommand("MLST " .. path)
|
||||||
|
if not response then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = response.content:match("^ ([^\r\n]+)\r\n$")
|
||||||
|
if not data then
|
||||||
|
return nil, "unparsable response"
|
||||||
|
end
|
||||||
|
|
||||||
|
info = FTP.parseFileInfo(data)
|
||||||
|
else
|
||||||
|
local result, reason = self:requestFilesInformation({ path })
|
||||||
|
if not result then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
info = result[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.cacheTimeout then
|
||||||
|
self:setCacheValue(cacheKey, info)
|
||||||
|
end
|
||||||
|
|
||||||
|
return info
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FTPFileExists(self, path, useCache)
|
||||||
|
local directory, filename = getParentDirectory(path)
|
||||||
|
if directory == "/" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local list, reason = self:listDirectory(directory or "", useCache)
|
||||||
|
if not list then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #list do
|
||||||
|
if list[i].name == filename then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Delete file or directory recursively
|
||||||
|
local function FTPRemoveFile(self, path, useCache)
|
||||||
|
local info, reason = self:getFileInfo(path, useCache)
|
||||||
|
if not info then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
if info.isdir then
|
||||||
|
local list = self:listDirectory(path, useCache)
|
||||||
|
for i = 1, #list do
|
||||||
|
if list[i].name ~= "." and list[i].name ~= ".." then
|
||||||
|
local success, reason = self:removeFile((path .. "/" .. list[i].name):gsub("/+", "/"))
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("RMD " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local response, reason = self:sendCommand("DELE " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create directory
|
||||||
|
local function FTPMakeDirectory(self, path)
|
||||||
|
local response, reason = self:sendCommand("MKD " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rename file
|
||||||
|
local function FTPRenameFile(self, from, to)
|
||||||
|
local response, reason = self:sendCommand("RNFR " .. from)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("RNTO " .. to)
|
||||||
|
if pizda(response) then
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Read file by path
|
||||||
|
-- Each received fragment is passed into callback function
|
||||||
|
local function FTPReadFile(self, path, callback, chunkSize)
|
||||||
|
local result, reason = self:beginNegotiation()
|
||||||
|
if not result then
|
||||||
|
return nil, "negotiation failed: " .. result
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("RETR " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
self:endNegotiation()
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local chunk, reason = self.dataSocket.read(chunkSize or math.huge)
|
||||||
|
if not chunk or #chunk == 0 then
|
||||||
|
self:endNegotiation()
|
||||||
|
|
||||||
|
if reason then
|
||||||
|
return nil, "unable to receive data: " .. reason
|
||||||
|
end
|
||||||
|
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, reason = pcall(callback, chunk)
|
||||||
|
if not success then
|
||||||
|
self:endNegotiation()
|
||||||
|
return nil, "callback error: " .. reason
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:endNegotiation()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read whole remote file into the memory
|
||||||
|
local function FTPReadFileToMemory(self, path)
|
||||||
|
local buffer = ""
|
||||||
|
local success, reason = self:readFile(
|
||||||
|
path,
|
||||||
|
function(chunk)
|
||||||
|
buffer = buffer .. chunk
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read whole remote file into the local filesystem
|
||||||
|
local function FTPReadFileToFilesystem(self, path, savePath)
|
||||||
|
local file = filesystem.open(savePath, "w")
|
||||||
|
if not file then
|
||||||
|
return nil, "could not open file for writing"
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, reason = self:readFile(
|
||||||
|
path,
|
||||||
|
function(chunk)
|
||||||
|
file:write(chunk)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
file:close()
|
||||||
|
return not not success, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write file
|
||||||
|
-- Data is obtained from the callback function until it returns nil
|
||||||
|
local function FTPWriteFile(self, path, callback)
|
||||||
|
local result, reason = self:beginNegotiation()
|
||||||
|
if not result then
|
||||||
|
return nil, "negotiation failed: " .. result
|
||||||
|
end
|
||||||
|
|
||||||
|
local response, reason = self:sendCommand("STOR " .. path)
|
||||||
|
if pizda(response) then
|
||||||
|
self:endNegotiation()
|
||||||
|
return cyka(response, reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local sucess, chunkOrReason = pcall(callback)
|
||||||
|
if not sucess then
|
||||||
|
self:endNegotiation()
|
||||||
|
return false, "callback error: " .. chunkOrReason
|
||||||
|
end
|
||||||
|
|
||||||
|
if not chunkOrReason then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
self:sendData(chunkOrReason)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:endNegotiation()
|
||||||
|
|
||||||
|
local result, reason = self:receiveResponse()
|
||||||
|
if not result or not result.ok then
|
||||||
|
return nil, reason or result.content
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write file from memory
|
||||||
|
local function FTPWriteFileFromMemory(self, path, data)
|
||||||
|
local chunkSize = 8192
|
||||||
|
local chunkIndex = 0
|
||||||
|
|
||||||
|
local result, reason = self:writeFile(
|
||||||
|
path,
|
||||||
|
function()
|
||||||
|
local chunk = data:sub(chunkIndex * chunkSize + 1, (chunkIndex + 1) * chunkSize)
|
||||||
|
chunkIndex = chunkIndex + 1
|
||||||
|
|
||||||
|
if #chunk == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write file from local filesystem
|
||||||
|
local function FTPWriteFileFromFilesystem(self, path, localPath)
|
||||||
|
local file, reason = filesystem.open(localPath, "r")
|
||||||
|
if not file then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
local result, reason = self:writeFile(
|
||||||
|
path,
|
||||||
|
function()
|
||||||
|
local chunk = file:read(math.huge)
|
||||||
|
if not chunk or #chunk == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
if not result then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Connect to the FTP server
|
||||||
|
function FTP.connect(address, port, responseTimeout)
|
||||||
|
local self = {}
|
||||||
|
|
||||||
|
self.commandChunkSize = 256
|
||||||
|
|
||||||
|
self.responseTimeout = responseTimeout or 1
|
||||||
|
self.cacheTimeout = 1
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
local controlSocket, reason = socketConnect(address, port, self.responseTimeout)
|
||||||
|
if not controlSocket then
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
|
||||||
|
self.controlSocket = controlSocket
|
||||||
|
|
||||||
|
self.receiveResponse = FTPReceiveResponse
|
||||||
|
self.sendCommand = FTPSendCommand
|
||||||
|
self.sendPassiveModeCommand = FTPSendPassiveModeCommand
|
||||||
|
self.beginNegotiation = FTPBeginNegotiation
|
||||||
|
self.endNegotiation = FTPEndNegotiation
|
||||||
|
self.receiveData = FTPReceiveData
|
||||||
|
self.sendData = FTPSendData
|
||||||
|
self.close = FTPClose
|
||||||
|
|
||||||
|
self.setCacheValue = FTPSetCacheValue
|
||||||
|
self.getCacheValue = FTPGetCacheValue
|
||||||
|
|
||||||
|
self.keepAlive = FTPKeepAlive
|
||||||
|
self.login = FTPLogin
|
||||||
|
self.setMode = FTPSetMode
|
||||||
|
self.getWorkingDirectory = FTPGetWorkingDirectory
|
||||||
|
self.changeWorkingDirectory = FTPChangeWorkingDirectory
|
||||||
|
self.getFileSize = FTPGetFileSize
|
||||||
|
self.resolvePath = FTPResolvePath
|
||||||
|
self.requestFilesInformation = FTPRequestFilesInformation
|
||||||
|
self.listDirectory = FTPListDirectory
|
||||||
|
self.getFileInfo = FTPGetFileInfo
|
||||||
|
self.fileExists = FTPFileExists
|
||||||
|
self.removeFile = FTPRemoveFile
|
||||||
|
self.makeDirectory = FTPMakeDirectory
|
||||||
|
self.renameFile = FTPRenameFile
|
||||||
|
|
||||||
|
self.readFile = FTPReadFile
|
||||||
|
self.readFileToMemory = FTPReadFileToMemory
|
||||||
|
self.readFileToFilesystem = FTPReadFileToFilesystem
|
||||||
|
self.writeFile = FTPWriteFile
|
||||||
|
self.writeFileFromMemory = FTPWriteFileFromMemory
|
||||||
|
self.writeFileFromFilesystem = FTPWriteFileFromFilesystem
|
||||||
|
|
||||||
|
-- Receiving greeting
|
||||||
|
local response, reason = self:receiveResponse()
|
||||||
|
if not response then
|
||||||
|
self:close()
|
||||||
|
return nil, "unable to receive greeting: " .. reason
|
||||||
|
end
|
||||||
|
|
||||||
|
if response.status ~= 220 then
|
||||||
|
self:close()
|
||||||
|
return nil, "the server is not able to process client: " .. response.content
|
||||||
|
end
|
||||||
|
|
||||||
|
self.greeting = response.content
|
||||||
|
|
||||||
|
-- Requesting and parsing server features
|
||||||
|
local response, reason = self:sendCommand("FEAT")
|
||||||
|
if pizda(response) then
|
||||||
|
self:close()
|
||||||
|
return nil, "unable to receive server features: " .. (reason or response.content)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.features = {}
|
||||||
|
for line in FTP.lines(response.content) do
|
||||||
|
local feature = line:match("^ (%w+)$")
|
||||||
|
|
||||||
|
if feature then
|
||||||
|
self.features[feature] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return FTP
|
||||||
Loading…
x
Reference in New Issue
Block a user