Advertisement
Guest User

ftp.lua

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