Advertisement
Guest User

ftp.lua

a guest
Jul 22nd, 2016
80
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.03 KB | None | 0 0
  1. local component = require("component")
  2.  
  3. if not component.isAvailable("internet") then
  4. io.stderr:write("OpenFTP requires an Internet Card to run!\n")
  5. return
  6. end
  7.  
  8. local internet = require("internet")
  9. local computer = require("computer")
  10. local unicode = require("unicode")
  11. local shell = require("shell")
  12. local event = require("event")
  13. local term = require("term")
  14. local text = require("text")
  15. local fs = require("filesystem")
  16. local inetc = component.internet
  17. local gpu = component.gpu
  18.  
  19. -- Variables -------------------------------------------------------------------
  20.  
  21. local isColored, isVerbose
  22. local args, options
  23.  
  24. local sock, host, port, timer
  25. local w = gpu.getResolution()
  26.  
  27. print("Term width: " .. tostring(w))
  28.  
  29. local history = {}
  30. local commands = {}
  31.  
  32. local chsize = 102400
  33. local isRunning = true
  34.  
  35. -- Functions -------------------------------------------------------------------
  36.  
  37. local function setFG(fg)
  38. if isColored then
  39. gpu.setForeground(fg)
  40. end
  41. end
  42.  
  43. local function nop() end
  44.  
  45. local function help()
  46. print("Usage: ftp [--colors=<always|never|auto>] <host> [port]")
  47. print()
  48. print("Options: ")
  49. print(" --colors=<always|never|auto> Specify whether to use color or not.")
  50.  
  51. os.exit(0)
  52. end
  53.  
  54. local function init(...)
  55. args, options = shell.parse(...)
  56.  
  57. local oColors = options["colors"] or "auto"
  58. oColors = (oColors == "always" or oColors == "never") and oColors or "auto"
  59.  
  60. if oColors == "always" then
  61. isColored = true
  62. elseif oColors == "never" then
  63. isColored = false
  64. elseif oColors == "auto" then
  65. isColored = gpu.getDepth() > 1
  66. end
  67.  
  68. isVerbose = options["verbose"] == true
  69. host, port = args[1] or nil, args[2] or 21
  70.  
  71. if #args < 1 then help() end
  72. end
  73.  
  74. local function connect()
  75. local lSock, reason = internet.open(host .. ":" .. port)
  76. if not lSock then
  77. io.stderr:write(("ftp: %s: %s\n"):format(host .. ":" .. port,
  78. reason or "unknown error"))
  79. os.exit(1)
  80. return
  81. end
  82.  
  83. sock = lSock
  84. sock:setTimeout(0.2)
  85. end
  86.  
  87. local function lost()
  88. setFG(0xFF0000)
  89. print("Connection lost.")
  90. setFG(0xFFFFFF)
  91. sock:close()
  92. os.exit(0)
  93. end
  94.  
  95. local function readLine()
  96. local ok, line = pcall(sock.read, sock)
  97. if ok and line == "" then lost() end
  98.  
  99. return ok and line or false
  100. end
  101.  
  102. local function readToEnd(f)
  103. local was = false
  104. local lastRet
  105. repeat
  106. local line = readLine()
  107. if line then
  108. lastRet = f(line)
  109. was = true
  110. end
  111. until not line and was
  112. return was, lastRet
  113. end
  114.  
  115. local function parseOutput(str)
  116. local match = {str:match("^(%d%d%d)([ -])(.*)$")}
  117.  
  118. if #match < 1 then return false end
  119.  
  120. local code = tonumber(match[1])
  121. local codeColor do
  122. if code >= 100 and code < 200 then
  123. codeColor = 0x0000FF
  124. elseif code >= 200 and code < 300 then
  125. codeColor = 0x00FF00
  126. elseif code >= 300 and code < 400 then
  127. codeColor = 0xFFFF00
  128. else
  129. codeColor = 0xFF0000
  130. end
  131. end
  132. local isLast = match[2] == " "
  133. local text = match[3]
  134.  
  135. return {
  136. code = code,
  137. codeColor = codeColor,
  138. isLast = isLast,
  139. text = text
  140. }
  141. end
  142.  
  143. local function traceLine(data)
  144. setFG(data.codeColor)
  145. io.write(data.code)
  146. setFG(0x666999)
  147. io.write(data.isLast and " " or "-")
  148. setFG(0xFFFFFF)
  149. io.write(data.text .. "\n")
  150. end
  151.  
  152. local function trace(str)
  153. local data = parseOutput(str)
  154. if data then
  155. traceLine(data)
  156. return data
  157. else
  158. print(str)
  159. end
  160. end
  161.  
  162. local function exit()
  163. event.cancel(timer)
  164.  
  165. if sock then
  166. sock:write("QUIT\r\n")
  167. sock:flush()
  168. readToEnd(trace, true)
  169. sock:close()
  170. end
  171.  
  172. setFG(0xFFFFFF)
  173.  
  174. os.exit(0)
  175. end
  176.  
  177. local function auth()
  178. timer = event.timer(1, function ()
  179. if isRunning and sock.stream.socket.finishConnect() then
  180. exit()
  181. end
  182. end, math.huge)
  183.  
  184. local got = false
  185. while true do
  186. local user repeat
  187. io.write("Name: ")
  188. user = term.read()
  189. if not user then
  190. print()
  191. end
  192. until user and #user > 0
  193. sock:write("USER " .. user .. "\r\n")
  194. sock:flush()
  195.  
  196. local got = false
  197.  
  198. readToEnd(function (str)
  199. local data = trace(str)
  200. got = data and data.code == 331 and data.isLast
  201. end)
  202.  
  203. if got then break end
  204. end
  205.  
  206. io.write("Password: ")
  207. pass = term.read(nil, nil, nil, "*"):sub(1, -2)
  208. if not pass then
  209. print()
  210. end
  211. sock:write("PASS " .. (pass or "") .. "\r\n")
  212. sock:flush()
  213.  
  214. local logined = false
  215.  
  216. while true do
  217. local was = readToEnd(function (str)
  218. local data = trace(str)
  219. logined = data and data.code == 230 and data.isLast
  220. end)
  221. if was then break end
  222. end
  223.  
  224. if not logined then
  225. exit()
  226. end
  227.  
  228. print("Using binary mode to transfer files.")
  229. sock:write("TYPE I\r\n")
  230. sock:flush()
  231. readToEnd(nop)
  232. end
  233.  
  234. local function pasv()
  235. sock:write("PASV\r\n")
  236. local ip, port
  237.  
  238. local was, ret = readToEnd(function (str)
  239. local data = trace(str)
  240. if data and data.code == 227 and data.isLast then
  241. local match = {data.text:match("(%d+),(%d+),(%d+),(%d+),(%d+),(%d+)")}
  242. if match then
  243. ip = table.concat({match[1], match[2], match[3], match[4]}, ".")
  244. port = tonumber(match[5]) * 256 + tonumber(match[6])
  245. end
  246. end
  247. end)
  248.  
  249. if not ip or not port then
  250. return false
  251. end
  252.  
  253. return inetc.connect(ip, port)
  254. end
  255.  
  256. local function readPasv(pasvSock, f)
  257. os.sleep(0.2)
  258.  
  259. local buf = {}
  260. local bufLen = 0
  261. local written = false
  262.  
  263. while true do
  264. local chunk = pasvSock.read(chsize)
  265.  
  266. if bufLen >= chsize and written then
  267. buf = {}
  268. bufLen = 0
  269. end
  270.  
  271. if chunk then
  272. table.insert(buf, chunk)
  273. bufLen = bufLen + #chunk
  274. written = false
  275. end
  276.  
  277. if not written and (bufLen >= chsize or not chunk) then
  278. f(table.concat(buf), bufLen)
  279. written = true
  280. end
  281.  
  282. if not chunk and written then break end
  283. end
  284.  
  285. pasvSock.close()
  286. end
  287.  
  288. local function writePasv(pasvSock, f)
  289. repeat
  290. local chunk, len = f()
  291. if chunk then
  292. len = len or 0
  293. local written = 0
  294. repeat
  295. written = written + pasvSock.write(chunk)
  296. until written >= len
  297. end
  298. until not chunk
  299.  
  300. pasvSock.write("")
  301. pasvSock.close()
  302.  
  303. os.sleep(0.2)
  304. end
  305.  
  306. local function handleInput(str)
  307. str = text.trim(str)
  308.  
  309. if str:sub(1, 1) == "!" then
  310. if str:sub(2, -1) == "" then
  311. shell.execute("sh")
  312. else
  313. shell.execute(str:sub(2, -1))
  314. end
  315.  
  316. return
  317. end
  318.  
  319. local cmd, args do
  320. local tokens = text.tokenize(str)
  321. if #tokens < 1 then return end
  322.  
  323. cmd = tokens[1]
  324. table.remove(tokens, 1)
  325. args = tokens
  326. end
  327.  
  328. if commands[cmd] then
  329. commands[cmd](args)
  330. else
  331. setFG(0xFF0000)
  332. print("Invalid command.")
  333. setFG(0xFFFFFF)
  334. end
  335. end
  336.  
  337. local function main()
  338. connect()
  339. auth()
  340.  
  341. repeat
  342. setFG(0x999999)
  343. io.write("ftp> ")
  344. setFG(0xFFFFFF)
  345. local input = term.read(history)
  346.  
  347. if input and text.trim(input) ~= "" then
  348. handleInput(input)
  349.  
  350. if input:sub(1, 1) ~= " " then
  351. table.insert(history, input)
  352. end
  353. end
  354. until not input
  355. print()
  356.  
  357. isRunning = false
  358. end
  359.  
  360. local progress do
  361. local units = {"B", "k", "M", "G", "T"}
  362. local chars = {[0] = ""}
  363. for i = 0x258f, 0x2588, -1 do
  364. table.insert(chars, unicode.char(i))
  365. end
  366.  
  367. local function formatFSize(fsize)
  368. local result = ""
  369. if fsize < 10e3 then
  370. result = ("%.4f"):format(fsize):sub(1,5)
  371. result = result .. (" "):rep(6 - #result) .. units[1]
  372. elseif fsize < 10e13 then
  373. local digits = #("%d"):format(fsize)
  374. local unit = units[math.floor((digits - 1) / 3) + 1]
  375. result = ("%.13f"):format(fsize / (10 ^ (math.floor((digits - 1) / 3) * 3))):sub(1, 5)
  376. result = result .. (" "):rep(6 - #result) .. unit
  377. else
  378. result = ("%.1e"):format(fsize)
  379. end
  380. return result
  381. end
  382.  
  383. function progress(fgot, ftotal, tstart, width)
  384. local perc = 0
  385. if tonumber(tostring(fgot / ftotal)) then
  386. perc = fgot / ftotal * 100
  387. if perc > 100 then
  388. perc = 100
  389. end
  390. else
  391. error("The programmer has derped! fgot = 0, ftotal = 0, perc = -nan, undefined behaviour!")
  392. end
  393. local pstr = ("%.2f"):format(perc)
  394.  
  395. local delta = computer.uptime() - tstart
  396. local perperc = delta / perc
  397. local t = (100 - perc) * perperc
  398. local time = "n/a"
  399. local cspeed = ""
  400. if tonumber(tostring(t)) then
  401. cspeed = tonumber(tostring(fgot / delta))
  402. cspeed = cspeed and (formatFSize(cspeed) .. "/s") or "n/a"
  403. local days = math.floor(t / 86400)
  404. t = t - days * 86400
  405. local hours = math.floor(t / 3600)
  406. t = t - hours * 3600
  407. local minutes = math.floor(t / 60)
  408. local seconds = t - minutes * 60
  409. time = ("%02d"):format(seconds)
  410. time = ("%02d"):format(minutes) .. ":" .. time
  411. if hours ~= 0 or days ~= 0 then
  412. time = ("%02d"):format(hours) .. ":" .. time
  413. end
  414. if days ~= 0 then
  415. time = tostring(days) .. "d, " .. time
  416. end
  417. end
  418.  
  419. local lpart = formatFSize(fgot) .. " / " .. formatFSize(ftotal) .. "▕"
  420. local rpart = chars[1] .. (" "):rep(6 - #pstr) .. pstr .. "% " .. cspeed .. " -- " .. time
  421. local pwidth = width - unicode.len(lpart) - unicode.len(rpart)
  422.  
  423. local cwidth = pwidth / 100
  424. local fr = tonumber(select(2, math.modf(perc * cwidth)))
  425. local lastblockindex = math.floor(fr * 8)
  426. if lastblockindex == 8 then
  427. lastblockindex = 7
  428. end
  429. local lastblock = chars[lastblockindex]
  430. local blocks = math.floor(perc * cwidth)
  431.  
  432. local result = lpart .. chars[8]:rep(blocks) .. lastblock
  433. result = result .. (" "):rep(width - unicode.len(result) - unicode.len(rpart)) .. rpart
  434. return result
  435. end
  436. end
  437.  
  438. -- Сommands --------------------------------------------------------------------
  439.  
  440. function commands.quit()
  441. exit()
  442. end
  443.  
  444. function commands.pwd()
  445. sock:write("PWD\r\n")
  446. sock:flush()
  447. readToEnd(trace)
  448. end
  449.  
  450. function commands.system()
  451. sock:write("SYST\r\n")
  452. sock:flush()
  453. readToEnd(trace)
  454. end
  455.  
  456. function commands.nop()
  457. sock:write("NOOP\r\n")
  458. sock:flush()
  459. readToEnd(trace)
  460. end
  461.  
  462. commands[".."] = function ()
  463. sock:write("CDUP\r\n")
  464. sock:flush()
  465. readToEnd(trace)
  466. end
  467.  
  468. function commands.size(args)
  469. if #args < 1 then
  470. print("Usage: size <file>")
  471. return
  472. end
  473.  
  474. sock:write("SIZE " .. args[1] .. "\r\n")
  475. sock:flush()
  476. readToEnd(trace)
  477. end
  478.  
  479. function commands.help(args)
  480. sock:write("HELP\r\n")
  481. sock:flush()
  482. readToEnd(trace)
  483. end
  484.  
  485. function commands.stat()
  486. sock:write("STAT\r\n")
  487. sock:flush()
  488. readToEnd(trace)
  489. end
  490.  
  491. function commands.binary()
  492. sock:write("TYPE I\r\n")
  493. sock:flush()
  494. readToEnd(trace)
  495. end
  496.  
  497. function commands.ascii()
  498. sock:write("TYPE A\r\n")
  499. sock:flush()
  500. readToEnd(trace)
  501. end
  502.  
  503. function commands.cd(args)
  504. if #args < 1 then
  505. print("Usage: cd <dir>")
  506. return
  507. end
  508.  
  509. sock:write("CWD " .. args[1] .. "\r\n")
  510. sock:flush()
  511. readToEnd(trace)
  512. end
  513.  
  514. function commands.ls(args)
  515. local pasvSock = pasv()
  516. if not pasvSock then
  517. return
  518. end
  519.  
  520. full = false
  521. for _, arg in ipairs(args) do
  522. if arg:sub(1, 1) == "-" then
  523. full = not not arg:match("l")
  524. end
  525. end
  526.  
  527. sock:write(full and "LIST\r\n" or "NLST\r\n")
  528. sock:flush()
  529. readToEnd(function (str)
  530. local data = trace(str)
  531. if data and data.code == 150 and data.isLast then
  532. readPasv(pasvSock, function (str) print(str) end)
  533. end
  534. end)
  535. end
  536.  
  537. function commands.raw(args)
  538. sock:write(table.concat(args, " ") .. "\r\n")
  539. sock:flush()
  540. os.sleep(0.5)
  541. readToEnd(trace, true)
  542. end
  543.  
  544. function commands.get(args)
  545. if #args < 1 then
  546. print("Usage: get <file> [local-path]")
  547. return
  548. end
  549.  
  550. local file, reason = io.open(args[2] or args[1], "w")
  551.  
  552. if not file then
  553. setFG(0xFF0000)
  554. print("Error opening file for writing: "
  555. .. tostring(reason or "unknown error"))
  556. setFG(0x000000)
  557. return
  558. end
  559.  
  560. local pasvSock = pasv()
  561. if not pasvSock then
  562. return
  563. end
  564.  
  565. sock:write("RETR " .. args[1] .. "\r\n")
  566. sock:flush()
  567. readToEnd(function (str)
  568. local data = trace(str)
  569. if data and data.code == 150 and data.isLast then
  570. local start = computer.uptime()
  571. local size = data.text:match("(%d+)%s+bytes")
  572. size = tonumber(size)
  573. local show = size and size > 0
  574. local now = 0
  575.  
  576. readPasv(pasvSock, function (chunk, len)
  577. file:write(chunk)
  578. if show then
  579. now = now + len
  580. term.clearLine()
  581. io.write(progress(now, size, start, w))
  582. end
  583. end)
  584.  
  585. if show then
  586. print()
  587. end
  588.  
  589. file:flush()
  590. file:close()
  591. end
  592. end)
  593. end
  594.  
  595. function commands.put(args)
  596. if #args < 1 then
  597. print("Usage: put <local-path> [file]")
  598. return
  599. end
  600.  
  601. local file, reason = io.open(args[1], "rb")
  602.  
  603. if not file then
  604. setFG(0xFF0000)
  605. print("Error opening file for reading: "
  606. .. tostring(reason or "unknown error"))
  607. setFG(0x000000)
  608. return
  609. end
  610.  
  611. local pasvSock = pasv()
  612. if not pasvSock then
  613. return
  614. end
  615.  
  616. sock:write("STOR " .. (args[2] or args[1]) .. "\r\n")
  617. sock:flush()
  618. readToEnd(function (str)
  619. local data = trace(str)
  620. if data and data.code == 150 and data.isLast then
  621. os.sleep(0.3)
  622.  
  623. local start = computer.uptime()
  624. local size = fs.size(os.getenv("PWD") .. "/" .. args[1])
  625. local now = 0
  626.  
  627. writePasv(pasvSock, function ()
  628. local chunk = file:read(chsize)
  629. if not chunk then
  630. return
  631. end
  632.  
  633. local len = #chunk
  634.  
  635. now = now + len
  636. term.clearLine()
  637. io.write(progress(now, size, start, w))
  638.  
  639. return chunk, len
  640. end)
  641.  
  642. print()
  643.  
  644. file:close()
  645. end
  646. end)
  647. end
  648.  
  649.  
  650. -- Main ------------------------------------------------------------------------
  651.  
  652. init(...)
  653. main()
  654. exit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement