View difference between Paste ID: YT6WBS3x and UPcwLA9e
SHOW: | | - or go back to the newest paste.
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 read
86
87
local function lost()
88
  read(trace, true)
89
  setFG(0xFF0000)
90
  print("Connection lost.")
91
  setFG(0xFFFFFF)
92
  sock:close()
93
  os.exit(0)
94
end
95
96
local function readLine()
97
  local ok, line = pcall(sock.read, sock)
98
  if ok and line == "" then lost() end
99
100
  return ok and line or false
101
end
102
103
function read(f, nwait)
104
  local was, lastRet, out = false, nil, computer.uptime() + 2
105
106
  repeat
107
    local line = readLine()
108
109
    if line then
110
      lastRet = f(line)
111
      was = true
112
      out = computer.uptime() + 2
113
    end
114
115
    if computer.uptime() >= out then
116
      lost()
117
    end
118
  until not line and (was or nwait)
119
120
  return was, lastRet
121
end
122
123
local function parseOutput(str)
124
  local match = {str:match("^(%d%d%d)([ -])(.*)$")}
125
126
  if #match < 1 then return false end
127
128
  local code = tonumber(match[1])
129
  local codeColor do
130
    if code >= 100 and code < 200 then
131
      codeColor = 0x0000FF
132
    elseif code >= 200 and code < 300 then
133
      codeColor = 0x00FF00
134
    elseif code >= 300 and code < 400 then
135
      codeColor = 0xFFFF00
136
    else
137
      codeColor = 0xFF0000
138
    end
139
  end
140
  local isLast = match[2] == " "
141
  local text = match[3]
142
143
  return {
144
    code = code,
145
    codeColor = codeColor,
146
    isLast = isLast,
147
    text = text
148
  }
149
end
150
151
local function traceLine(data)
152
  setFG(data.codeColor)
153
  io.write(data.code)
154
  setFG(0x666999)
155
  io.write(data.isLast and " " or "-")
156
  setFG(0xFFFFFF)
157
  io.write(data.text .. "\n")
158
end
159
160
local function trace(str)
161
  local data = parseOutput(str)
162
  if data then
163
    traceLine(data)
164
    return data
165
  else
166
    print(str)
167
  end
168
end
169
170
local function exit()
171
  if sock then
172
    sock:write("QUIT\r\n")
173
    sock:flush()
174
    read(trace, true)
175
    sock:close()
176
  end
177
178
  setFG(0xFFFFFF)
179
180
  os.exit(0)
181
end
182
183
local function auth()
184
  read(trace)
185
186
  local got = false
187
  while true do
188
    local user repeat
189
      io.write("Name: ")
190
      user = term.read()
191
      if not user then
192
        print()
193
      end
194
    until user and #user > 0
195
    sock:write("USER " .. user .. "\r\n")
196
    sock:flush()
197
198
    local got = false
199
200
    read(function (str)
201
      local data = trace(str)
202
      if not got then
203
        got = data and data.code == 331 and data.isLast
204
      end
205
    end)
206
207
    if got then break end
208
  end
209
210
  io.write("Password: ")
211
  pass = term.read(nil, nil, nil, "*"):sub(1, -2)
212
  if not pass then
213
    print()
214
  end
215
  sock:write("PASS " .. (pass or "") .. "\r\n")
216
  sock:flush()
217
218
  local logined = false
219
220
  while true do
221
    local was = read(function (str)
222
      local data = trace(str)
223
      logined = data and data.code == 230 and data.isLast
224
    end)
225
    if was then break end
226
  end
227
228
  if not logined then
229
    exit()
230
  end
231
232
  print("Using binary mode to transfer files.")
233
  sock:write("TYPE I\r\n")
234
  sock:flush()
235
  read(nop)
236
end
237
238
local function pasv()
239
  sock:write("PASV\r\n")
240
  local ip, port
241
242
  local was, ret = read(function (str)
243
    local data = trace(str)
244
    if data and data.code == 227 and data.isLast then
245
      local match = {data.text:match("(%d+),(%d+),(%d+),(%d+),(%d+),(%d+)")}
246
      if match then
247
        ip = table.concat({match[1], match[2], match[3], match[4]}, ".")
248
        port = tonumber(match[5]) * 256 + tonumber(match[6])
249
      end
250
    end
251
  end)
252
253
  if not ip or not port then
254
    return false
255
  end
256
257
  return inetc.connect(ip, port)
258
end
259
260
local function readPasv(pasvSock, f)
261
  os.sleep(0.2)
262
263
  local buf = {}
264
  local bufLen = 0
265
  local written = false
266
267
  while true do
268
    local chunk = pasvSock.read(chsize)
269
270
    if bufLen >= chsize and written then
271
      buf = {}
272
      bufLen = 0
273
    end
274
275
    if chunk then
276
      table.insert(buf, chunk)
277
      bufLen = bufLen + #chunk
278
      written = false
279
    end
280
281
    if not written and (bufLen >= chsize or not chunk) then
282
      f(table.concat(buf), bufLen)
283
      written = true
284
    end
285
286
    if not chunk and written then break end
287
  end
288
289
  pasvSock.close()
290
end
291
292
local function writePasv(pasvSock, f)
293
  repeat
294
    local chunk, len = f()
295
    if chunk then
296
      len = len or 0
297
      local written = 0
298
      repeat
299
        written = written + pasvSock.write(chunk)
300
      until written >= len
301
    end
302
  until not chunk
303
304
  pasvSock.write("")
305
  pasvSock.close()
306
307
  os.sleep(0.2)
308
end
309
310
local function handleInput(str)
311
  str = text.trim(str)
312
313
  if str:sub(1, 1) == "!" then
314
    if str:sub(2, -1)  == "" then
315
      shell.execute("sh")
316
    else
317
      shell.execute(str:sub(2, -1))
318
    end
319
320
    return
321
  end
322
323
  local cmd, args do
324
    local tokens = text.tokenize(str)
325
    if #tokens < 1 then return end
326
327
    cmd = tokens[1]
328
    table.remove(tokens, 1)
329
    args = tokens
330
  end
331
332
  if commands[cmd] then
333
    commands[cmd](args)
334
  else
335
    setFG(0xFF0000)
336
    print("Invalid command.")
337
    setFG(0xFFFFFF)
338
  end
339
end
340
341
local function main()
342
  connect()
343
  auth()
344
345
  repeat
346
    setFG(0x999999)
347
    io.write("ftp> ")
348
    setFG(0xFFFFFF)
349
    local input = term.read(history)
350
351
    if input and text.trim(input) ~= "" then
352
      handleInput(input)
353
354
      if input:sub(1, 1) ~= " " then
355
        table.insert(history, input)
356
      end
357
    end
358
  until not input
359
  print()
360
361
  isRunning = false
362
end
363
364
local progress do
365
  local units = {"B", "k", "M", "G", "T"}
366
  local chars = {[0] = ""}
367
  for i = 0x258f, 0x2588, -1 do
368
    table.insert(chars, unicode.char(i))
369
  end
370
371
  local function formatFSize(fsize)
372
    local result = ""
373
    if fsize < 10e3 then
374
      result = ("%.4f"):format(fsize):sub(1,5)
375
      result = result .. (" "):rep(6 - #result) .. units[1]
376
    elseif fsize < 10e13 then
377
      local digits = #("%d"):format(fsize)
378
      local unit = units[math.floor((digits - 1) / 3) + 1]
379
      result = ("%.13f"):format(fsize / (10 ^ (math.floor((digits - 1) / 3) * 3))):sub(1, 5)
380
      result = result .. (" "):rep(6 - #result) .. unit
381
    else
382
      result = ("%.1e"):format(fsize)
383
    end
384
    return result
385
  end
386
387
  function progress(fgot, ftotal, tstart, width)
388
    local perc = 0
389
    if tonumber(tostring(fgot / ftotal)) then
390
      perc = fgot / ftotal * 100
391
      if perc > 100 then
392
        perc = 100
393
      end
394
    else
395
      error("The programmer has derped! fgot = 0, ftotal = 0, perc = -nan, undefined behaviour!")
396
    end
397
    local pstr = ("%.2f"):format(perc)
398
399
    local delta = computer.uptime() - tstart
400
    local perperc = delta / perc
401
    local t = (100 - perc) * perperc
402
    local time = "n/a"
403
    local cspeed = ""
404
    if tonumber(tostring(t)) then
405
      cspeed = tonumber(tostring(fgot / delta))
406
      cspeed = cspeed and (formatFSize(cspeed) .. "/s") or "n/a"
407
      local days = math.floor(t / 86400)
408
      t = t - days * 86400
409
      local hours = math.floor(t / 3600)
410
      t = t - hours * 3600
411
      local minutes = math.floor(t / 60)
412
      local seconds = t - minutes * 60
413
      time = ("%02d"):format(seconds)
414
      time = ("%02d"):format(minutes) .. ":" .. time
415
      if hours ~= 0 or days ~= 0 then
416
        time = ("%02d"):format(hours) .. ":" .. time
417
      end
418
      if days ~= 0 then
419
        time = tostring(days) .. "d, " .. time
420
      end
421
    end
422
423
    local lpart = formatFSize(fgot) .. " / " .. formatFSize(ftotal) .. "β–•"
424
    local rpart = chars[1] .. (" "):rep(6 - #pstr) .. pstr .. "% " .. cspeed .. " -- " .. time
425
    local pwidth = width - unicode.len(lpart) - unicode.len(rpart)
426
427
    local cwidth = pwidth / 100
428
    local fr = tonumber(select(2, math.modf(perc * cwidth)))
429
    local lastblockindex = math.floor(fr * 8)
430
    if lastblockindex == 8 then
431
      lastblockindex = 7
432
    end
433
    local lastblock = chars[lastblockindex]
434
    local blocks = math.floor(perc * cwidth)
435
436
    local result = lpart .. chars[8]:rep(blocks) .. lastblock
437
    result = result .. (" "):rep(width - unicode.len(result) - unicode.len(rpart)) .. rpart
438
    return result
439
  end
440
end
441
442
-- Π‘ommands --------------------------------------------------------------------
443
444
function commands.quit()
445
  exit()
446
end
447
448
function commands.pwd(args)
449
  local _, copts = shell.parse(table.unpack(args))
450
  if copts.help or copts.h then
451
    print("pwd - print working directory.")
452
    print()
453
    print("Usage: pwd [-h|--help]")
454
    print()
455
    print("Options:")
456
    print("  -h|--help  Print this message")
457
458
    return
459
  end
460
461
  sock:write("PWD\r\n")
462
  sock:flush()
463
  read(trace)
464
end
465
466
function commands.system(args)
467
  local _, copts = shell.parse(table.unpack(args))
468
  if copts.help or copts.h then
469
    print("system - print system running on server.")
470
    print()
471
    print("Usage: system [-h|--help]")
472
    print()
473
    print("Options:")
474
    print("  -h|--help  Print this message")
475
476
    return
477
  end
478
479
  sock:write("SYST\r\n")
480
  sock:flush()
481
  read(trace)
482
end
483
484
function commands.nop(args)
485
  local _, copts = shell.parse(table.unpack(args))
486
  if copts.help or copts.h then
487
    print("nop - do nothing.")
488
    print()
489
    print("Usage: system [-h|--help]")
490
    print()
491
    print("Options:")
492
    print("  -h|--help  Print this message")
493
494
    return
495
  end
496
497
  sock:write("NOOP\r\n")
498
  sock:flush()
499
  read(trace)
500
end
501
502
commands[".."] = function (args)
503
  local _, copts = shell.parse(table.unpack(args))
504
  if copts.help or copts.h then
505
    print(".. - go to the parent directory.")
506
    print()
507
    print("Usage: .. [-h|--help]")
508
    print("Options:")
509
    print("  -h|--help  Print this message")
510
511
    return
512
  end
513
514
  sock:write("CDUP\r\n")
515
  sock:flush()
516
  read(trace)
517
end
518
519
function commands.size(args)
520
  local _, copts = shell.parse(table.unpack(args))
521
  if copts.help or copts.h then
522
    print("size - print file size.")
523
    print()
524
    print("Usage: size [-h|--help] <path>")
525
    print()
526
    print("Options:")
527
    print("  -h|--help  Print this message")
528
    print()
529
    print("Arguments:")
530
    print("  path  Path to file.")
531
532
    return
533
  end
534
535
  if #args < 1 then
536
    print("Usage: size <file>")
537
    return
538
  end
539
540
  sock:write("SIZE " .. args[1] .. "\r\n")
541
  sock:flush()
542
  read(trace)
543
end
544
545
function commands.shelp(args)
546
  local _, copts = shell.parse(table.unpack(args))
547
  if copts.help or copts.h then
548
    print("shelp - print server help.")
549
    print()
550
    print("Usage: shelp [-h|--help]")
551
    print()
552
    print("Options:")
553
    print("  -h|--help  Print this message")
554
555
    return
556
  end
557
558
  sock:write("HELP\r\n")
559
  sock:flush()
560
  read(trace)
561
end
562
563
function commands.stat(args)
564
  local _, copts = shell.parse(table.unpack(args))
565
  if copts.help or copts.h then
566
    print("stat - print server statistics.")
567
    print()
568
    print("Usage: stat [-h|--help]")
569
    print()
570
    print("Options:")
571
    print("  -h|--help  Print this message")
572
573
    return
574
  end
575
576
  sock:write("STAT\r\n")
577
  sock:flush()
578
  read(trace)
579
end
580
581
function commands.binary(args)
582
  local _, copts = shell.parse(table.unpack(args))
583
  if copts.help or copts.h then
584
    print("binary - switch to binary mode.")
585
    print()
586
    print("Usage: binary [-h|--help]")
587
    print()
588
    print("Options:")
589
    print("  -h|--help  Print this message")
590
591
    return
592
  end
593
594
  sock:write("TYPE I\r\n")
595
  sock:flush()
596
  read(trace)
597
end
598
599
function commands.ascii()
600
  local _, copts = shell.parse(table.unpack(args))
601
  if copts.help or copts.h then
602
    print("ascii - switch to ASCII mode.")
603
    print()
604
    print("Usage: ascii [-h|--help]")
605
    print()
606
    print("Options:")
607
    print("  -h|--help  Print this message")
608
609
    return
610
  end
611
612
  sock:write("TYPE A\r\n")
613
  sock:flush()
614
  read(trace)
615
end
616
617
function commands.cd(args)
618
  local cargs, copts = shell.parse(table.unpack(args))
619
  if copts.help or copts.h or #cargs < 1 then
620
    print("ascii - change working directory.")
621
    print()
622
    print("Usage: ascii [-h|--help] <path>")
623
    print()
624
    print("Options:")
625
    print("  -h|--help  Print this message")
626
    print()
627
    print("Arguments:")
628
    print("  path  Path to new working directory")
629
630
    return
631
  end
632
633
  sock:write("CWD " .. cargs[1] .. "\r\n")
634
  sock:flush()
635
  read(trace)
636
end
637
638
function commands.ls(args)
639
  local cargs, copts = shell.parse(table.unpack(args))
640
  if copts.help or copts.h then
641
    print("ls - print list files.")
642
    print()
643
    print("Usage: ls [-h|--help] [path]")
644
    print()
645
    print("Options:")
646
    print("  -h|--help  Print this message")
647
    print("  -s         Use short listing format")
648
    print()
649
    print("Arguments:")
650
    print("  path  Path to the directory (the current")
651
    print("        directory by default) to list its files")
652
653
    return
654
  end
655
656
  local pasvSock = pasv()
657
  if not pasvSock then
658
    return
659
  end
660
661
  sock:write((copts.s and "NLST" or "LIST")
662
             .. (cargs[1] and " " .. cargs[1] or "") .. "\r\n")
663
  sock:flush()
664
  read(function (str)
665
    local data = trace(str)
666
    if data and data.code == 150 and data.isLast then
667
      readPasv(pasvSock, function (str) print(str) end)
668
    end
669
  end)
670
end
671
672
function commands.rename(args)
673
  local cargs, copts = shell.parse(table.unpack(args))
674
  if copts.help or copts.h or #cargs < 2 then
675
    print("rename - rename file or directory.")
676
    print()
677
    print("Usage: rename [-h|--help] <source> <dest>")
678
    print()
679
    print("Options:")
680
    print("  -h|--help  Print this message")
681
    print()
682
    print("Arguments:")
683
    print("  source  A path to file to rename")
684
    print("  dest    A name the source should be renamed to")
685
    print()
686
    print("Aliases: rn")
687
688
    return
689
  end
690
691
  sock:write("RNFR " .. cargs[1] .. "\r\n")
692
  sock:flush()
693
  read(function (str)
694
    local data = trace(str)
695
    if data and data.code == 350 and data.isLast then
696
      sock:write("RNTO " .. cargs[2] .. "\r\n")
697
      sock:flush()
698
      read(trace)
699
    end
700
  end)
701
end
702
703
commands.rn = commands.rename
704
705
function commands.rm(args)
706
  local cargs, copts = shell.parse(table.unpack(args))
707
  if copts.help or copts.h or #cargs < 1 then
708
    print("rm - remove a file or directory.")
709
    print()
710
    print("Usage: rm [-h|--help] [-d] <path>")
711
    print()
712
    print("Options:")
713
    print("  -h|--help  Print this message")
714
    print("  -d         Remove a directory")
715
    print()
716
    print("Arguments:")
717
    print("  path  A path to the file or directory to remove")
718
719
    return
720
  end
721
722
  sock:write((copts.d and "RMD " or "DELE ") .. cargs[1] .. "\r\n")
723
  sock:flush()
724
  read(trace)
725
end
726
727
function commands.mkdir(args)
728
  local cargs, copts = shell.parse(table.unpack(args))
729
  if copts.help or copts.h or #cargs < 1 then
730
    print("mkdir - create a directory.")
731
    print()
732
    print("Usage: mkdir [-h|--help] <path>")
733
    print()
734
    print("Options:")
735
    print("  -h|--help  Print this message")
736
    print()
737
    print("Arguments:")
738
    print("  path  A path to new directory")
739
740
    return
741
  end
742
743
  sock:write("MKD " .. cargs[1] .. "\r\n")
744
  sock:flush()
745
  read(trace)
746
end
747
748
function commands.raw(args)
749
  local cargs, copts = shell.parse(table.unpack(args))
750
  if copts.help or copts.h or #cargs < 1 then
751
    print("raw - send a message to the server.")
752
    print("Sent message will end with CRLF (\\r\\n)")
753
    print()
754
    print("Usage: raw [-h|--help] <message>")
755
    print()
756
    print("Options:")
757
    print("  -h|--help  Print this message")
758
    print()
759
    print("Arguments:")
760
    print("  message  Message that should be sent to the server")
761
762
    return
763
  end
764
765
  sock:write(cargs[1] .. "\r\n")
766
  sock:flush()
767
  os.sleep(0.5)
768
  read(trace, true)
769
end
770
771
function commands.get(args)
772
  local cargs, copts = shell.parse(table.unpack(args))
773
  if copts.help or copts.h or #cargs < 1 then
774
    print("get - get a file from the server.")
775
    print()
776
    print("Usage: get [-h|--help] <remote-path> [local-path]")
777
    print()
778
    print("Options:")
779
    print("  -h|--help  Print this message")
780
    print()
781
    print("Arguments:")
782
    print("  remote-path  A path to file that should be got from the server")
783
    print("  local-path   A path to local file (remote-path by default)")
784
785
    return
786
  end
787
788
  local file, reason = io.open(cargs[2] or cargs[1], "w")
789
790
  if not file then
791
    setFG(0xFF0000)
792
    print("Error opening file for writing: "
793
          .. tostring(reason or "unknown error"))
794
    setFG(0x000000)
795
    return
796
  end
797
798
  sock:write("SIZE " .. cargs[1] .. "\r\n")
799
  sock:flush()
800
801
  local size = 0
802
  read(function (str)
803
    local data = trace(str)
804
    if data and data.code == 213 and data.isLast then
805
      size = data.text:match("(%d+)")
806
    end
807
  end)
808
  size = tonumber(size)
809
810
  local pasvSock = pasv()
811
  if not pasvSock then
812
    return
813
  end
814
815
  sock:write("RETR " .. cargs[1] .. "\r\n")
816
  sock:flush()
817
  read(function (str)
818
    local data = trace(str)
819
    if data and data.code == 150 and data.isLast then
820
      local start = computer.uptime()
821
      local show = size and size > 0
822
      local now = 0
823
824
      io.write(progress(now, size, start, w))
825
826
      readPasv(pasvSock, function (chunk, len)
827
        file:write(chunk)
828
        if show then
829
          now = now + len
830
          term.clearLine()
831
          io.write(progress(now, size, start, w))
832
        end
833
      end)
834
835
      if show then
836
        print()
837
      end
838
839
      file:flush()
840
      file:close()
841
    end
842
  end)
843
end
844
845
function commands.put(args)
846
  local cargs, copts = shell.parse(table.unpack(args))
847
  if copts.help or copts.h or #cargs < 1 then
848
    print("put - put a file to the server.")
849
    print()
850
    print("Usage: put [-h|--help] <local-path> [remote-path]")
851
    print()
852
    print("Options:")
853
    print("  -h|--help  Print this message")
854
    print()
855
    print("Arguments:")
856
    print("  local-path   A path to local file.")
857
    print("  remote-path  A path to file that should be put to the server")
858
    print("               (local-path by default)")
859
860
    return
861
  end
862
863
  local file, reason = io.open(cargs[1], "rb")
864
865
  if not file then
866
    setFG(0xFF0000)
867
    print("Error opening file for reading: "
868
          .. tostring(reason or "unknown error"))
869
    setFG(0x000000)
870
    return
871
  end
872
873
  local pasvSock = pasv()
874
  if not pasvSock then
875
    return
876
  end
877
878
  sock:write("STOR " .. (cargs[2] or cargs[1]) .. "\r\n")
879
  sock:flush()
880
  read(function (str)
881
    local data = trace(str)
882
    if data and data.code == 150 and data.isLast then
883
      os.sleep(0.3)
884
885
      local start = computer.uptime()
886
      local size = fs.size(os.getenv("PWD") .. "/" .. cargs[1])
887
      local now = 0
888
889
      io.write(progress(now, size, start, w))
890
891
      writePasv(pasvSock, function ()
892
        local chunk = file:read(chsize)
893
        if not chunk then
894
          return
895
        end
896
897
        local len = #chunk
898
899
        now = now + len
900
        term.clearLine()
901
        io.write(progress(now, size, start, w))
902
903
        return chunk, len
904
      end)
905
906
      print()
907
908
      file:close()
909
    end
910
  end)
911
end
912
913
914
-- Main ------------------------------------------------------------------------
915
916
init(...)
917
main()
918
exit()