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.08 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. print(require("serialization").serialize(chars))
  368.  
  369. local function formatFSize(fsize)
  370. local result = ""
  371. if fsize < 10e3 then
  372. result = ("%.4f"):format(fsize):sub(1,5)
  373. result = result .. (" "):rep(6 - #result) .. units[1]
  374. elseif fsize < 10e13 then
  375. local digits = #("%d"):format(fsize)
  376. local unit = units[math.floor((digits - 1) / 3) + 1]
  377. result = ("%.13f"):format(fsize / (10 ^ (math.floor((digits - 1) / 3) * 3))):sub(1, 5)
  378. result = result .. (" "):rep(6 - #result) .. unit
  379. else
  380. result = ("%.1e"):format(fsize)
  381. end
  382. return result
  383. end
  384.  
  385. function progress(fgot, ftotal, tstart, width)
  386. local perc = 0
  387. if tonumber(tostring(fgot / ftotal)) then
  388. perc = fgot / ftotal * 100
  389. if perc > 100 then
  390. perc = 100
  391. end
  392. else
  393. error("The programmer has derped! fgot = 0, ftotal = 0, perc = -nan, undefined behaviour!")
  394. end
  395. local pstr = ("%.2f"):format(perc)
  396.  
  397. local delta = computer.uptime() - tstart
  398. local perperc = delta / perc
  399. local t = (100 - perc) * perperc
  400. local time = "n/a"
  401. local cspeed = ""
  402. if tonumber(tostring(t)) then
  403. cspeed = tonumber(tostring(fgot / delta))
  404. cspeed = cspeed and (formatFSize(cspeed) .. "/s") or "n/a"
  405. local days = math.floor(t / 86400)
  406. t = t - days * 86400
  407. local hours = math.floor(t / 3600)
  408. t = t - hours * 3600
  409. local minutes = math.floor(t / 60)
  410. local seconds = t - minutes * 60
  411. time = ("%02d"):format(seconds)
  412. time = ("%02d"):format(minutes) .. ":" .. time
  413. if hours ~= 0 or days ~= 0 then
  414. time = ("%02d"):format(hours) .. ":" .. time
  415. end
  416. if days ~= 0 then
  417. time = tostring(days) .. "d, " .. time
  418. end
  419. end
  420.  
  421. local lpart = formatFSize(fgot) .. " / " .. formatFSize(ftotal) .. "▕"
  422. local rpart = chars[1] .. (" "):rep(6 - #pstr) .. pstr .. "% " .. cspeed .. " -- " .. time
  423. local pwidth = width - unicode.len(lpart) - unicode.len(rpart)
  424.  
  425. local cwidth = pwidth / 100
  426. local fr = tonumber(select(2, math.modf(perc * cwidth)))
  427. local lastblockindex = math.floor(fr * 8)
  428. if lastblockindex == 8 then
  429. lastblockindex = 7
  430. end
  431. local lastblock = chars[lastblockindex]
  432. local blocks = math.floor(perc * cwidth)
  433.  
  434. local result = lpart .. chars[8]:rep(blocks) .. lastblock
  435. result = result .. (" "):rep(width - unicode.len(result) - unicode.len(rpart)) .. rpart
  436. return result
  437. end
  438. end
  439.  
  440. -- Сommands --------------------------------------------------------------------
  441.  
  442. function commands.quit()
  443. exit()
  444. end
  445.  
  446. function commands.pwd()
  447. sock:write("PWD\r\n")
  448. sock:flush()
  449. readToEnd(trace)
  450. end
  451.  
  452. function commands.system()
  453. sock:write("SYST\r\n")
  454. sock:flush()
  455. readToEnd(trace)
  456. end
  457.  
  458. function commands.nop()
  459. sock:write("NOOP\r\n")
  460. sock:flush()
  461. readToEnd(trace)
  462. end
  463.  
  464. commands[".."] = function ()
  465. sock:write("CDUP\r\n")
  466. sock:flush()
  467. readToEnd(trace)
  468. end
  469.  
  470. function commands.size(args)
  471. if #args < 1 then
  472. print("Usage: size <file>")
  473. return
  474. end
  475.  
  476. sock:write("SIZE " .. args[1] .. "\r\n")
  477. sock:flush()
  478. readToEnd(trace)
  479. end
  480.  
  481. function commands.help(args)
  482. sock:write("HELP\r\n")
  483. sock:flush()
  484. readToEnd(trace)
  485. end
  486.  
  487. function commands.stat()
  488. sock:write("STAT\r\n")
  489. sock:flush()
  490. readToEnd(trace)
  491. end
  492.  
  493. function commands.binary()
  494. sock:write("TYPE I\r\n")
  495. sock:flush()
  496. readToEnd(trace)
  497. end
  498.  
  499. function commands.ascii()
  500. sock:write("TYPE A\r\n")
  501. sock:flush()
  502. readToEnd(trace)
  503. end
  504.  
  505. function commands.cd(args)
  506. if #args < 1 then
  507. print("Usage: cd <dir>")
  508. return
  509. end
  510.  
  511. sock:write("CWD " .. args[1] .. "\r\n")
  512. sock:flush()
  513. readToEnd(trace)
  514. end
  515.  
  516. function commands.ls(args)
  517. local pasvSock = pasv()
  518. if not pasvSock then
  519. return
  520. end
  521.  
  522. full = false
  523. for _, arg in ipairs(args) do
  524. if arg:sub(1, 1) == "-" then
  525. full = not not arg:match("l")
  526. end
  527. end
  528.  
  529. sock:write(full and "LIST\r\n" or "NLST\r\n")
  530. sock:flush()
  531. readToEnd(function (str)
  532. local data = trace(str)
  533. if data and data.code == 150 and data.isLast then
  534. readPasv(pasvSock, function (str) print(str) end)
  535. end
  536. end)
  537. end
  538.  
  539. function commands.raw(args)
  540. sock:write(table.concat(args, " ") .. "\r\n")
  541. sock:flush()
  542. os.sleep(0.5)
  543. readToEnd(trace, true)
  544. end
  545.  
  546. function commands.get(args)
  547. if #args < 1 then
  548. print("Usage: get <file> [local-path]")
  549. return
  550. end
  551.  
  552. local file, reason = io.open(args[2] or args[1], "w")
  553.  
  554. if not file then
  555. setFG(0xFF0000)
  556. print("Error opening file for writing: "
  557. .. tostring(reason or "unknown error"))
  558. setFG(0x000000)
  559. return
  560. end
  561.  
  562. local pasvSock = pasv()
  563. if not pasvSock then
  564. return
  565. end
  566.  
  567. sock:write("RETR " .. args[1] .. "\r\n")
  568. sock:flush()
  569. readToEnd(function (str)
  570. local data = trace(str)
  571. if data and data.code == 150 and data.isLast then
  572. local start = computer.uptime()
  573. local size = data.text:match("(%d+)%s+bytes")
  574. size = tonumber(size)
  575. local show = size and size > 0
  576. local now = 0
  577.  
  578. readPasv(pasvSock, function (chunk, len)
  579. file:write(chunk)
  580. if show then
  581. now = now + len
  582. term.clearLine()
  583. io.write(progress(now, size, start, w))
  584. end
  585. end)
  586.  
  587. if show then
  588. print()
  589. end
  590.  
  591. file:flush()
  592. file:close()
  593. end
  594. end)
  595. end
  596.  
  597. function commands.put(args)
  598. if #args < 1 then
  599. print("Usage: put <local-path> [file]")
  600. return
  601. end
  602.  
  603. local file, reason = io.open(args[1], "rb")
  604.  
  605. if not file then
  606. setFG(0xFF0000)
  607. print("Error opening file for reading: "
  608. .. tostring(reason or "unknown error"))
  609. setFG(0x000000)
  610. return
  611. end
  612.  
  613. local pasvSock = pasv()
  614. if not pasvSock then
  615. return
  616. end
  617.  
  618. sock:write("STOR " .. (args[2] or args[1]) .. "\r\n")
  619. sock:flush()
  620. readToEnd(function (str)
  621. local data = trace(str)
  622. if data and data.code == 150 and data.isLast then
  623. os.sleep(0.3)
  624.  
  625. local start = computer.uptime()
  626. local size = fs.size(os.getenv("PWD") .. "/" .. args[1])
  627. local now = 0
  628.  
  629. writePasv(pasvSock, function ()
  630. local chunk = file:read(chsize)
  631. if not chunk then
  632. return
  633. end
  634.  
  635. local len = #chunk
  636.  
  637. now = now + len
  638. term.clearLine()
  639. io.write(progress(now, size, start, w))
  640.  
  641. return chunk, len
  642. end)
  643.  
  644. print()
  645.  
  646. file:close()
  647. end
  648. end)
  649. end
  650.  
  651.  
  652. -- Main ------------------------------------------------------------------------
  653.  
  654. init(...)
  655. main()
  656. exit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement