ftpclient.nim 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Dominik Picheta
  5. # See the file "copying.txt", included in this
  6. # distribution, for details about the copyright.
  7. #
  8. include "system/inclrtl"
  9. import sockets, strutils, parseutils, times, os, asyncio
  10. from asyncnet import nil
  11. from nativesockets import nil
  12. from asyncdispatch import Future
  13. ## **Note**: This module is deprecated since version 0.11.3.
  14. ## You should use the async version of this module
  15. ## `asyncftpclient <asyncftpclient.html>`_.
  16. ##
  17. ## ----
  18. ##
  19. ## This module **partially** implements an FTP client as specified
  20. ## by `RFC 959 <http://tools.ietf.org/html/rfc959>`_.
  21. ##
  22. ## This module provides both a synchronous and asynchronous implementation.
  23. ## The asynchronous implementation requires you to use the ``asyncFTPClient``
  24. ## function. You are then required to register the ``AsyncFTPClient`` with a
  25. ## asyncio dispatcher using the ``register`` function. Take a look at the
  26. ## asyncio module documentation for more information.
  27. ##
  28. ## **Note**: The asynchronous implementation is only asynchronous for long
  29. ## file transfers, calls to functions which use the command socket will block.
  30. ##
  31. ## Here is some example usage of this module:
  32. ##
  33. ## .. code-block:: Nim
  34. ## var ftp = ftpClient("example.org", user = "user", pass = "pass")
  35. ## ftp.connect()
  36. ## ftp.retrFile("file.ext", "file.ext")
  37. ##
  38. ## **Warning:** The API of this module is unstable, and therefore is subject
  39. ## to change.
  40. {.deprecated.}
  41. type
  42. FtpBase*[SockType] = ref FtpBaseObj[SockType]
  43. FtpBaseObj*[SockType] = object
  44. csock*: SockType
  45. dsock*: SockType
  46. when SockType is asyncio.AsyncSocket:
  47. handleEvent*: proc (ftp: AsyncFTPClient, ev: FTPEvent){.closure,gcsafe.}
  48. disp: Dispatcher
  49. asyncDSockID: Delegate
  50. user*, pass*: string
  51. address*: string
  52. when SockType is asyncnet.AsyncSocket:
  53. port*: nativesockets.Port
  54. else:
  55. port*: Port
  56. jobInProgress*: bool
  57. job*: FTPJob[SockType]
  58. dsockConnected*: bool
  59. FTPJobType* = enum
  60. JRetrText, JRetr, JStore
  61. FtpJob[T] = ref FtpJobObj[T]
  62. FTPJobObj[T] = object
  63. prc: proc (ftp: FTPBase[T], async: bool): bool {.nimcall, gcsafe.}
  64. case typ*: FTPJobType
  65. of JRetrText:
  66. lines: string
  67. of JRetr, JStore:
  68. file: File
  69. filename: string
  70. total: BiggestInt # In bytes.
  71. progress: BiggestInt # In bytes.
  72. oneSecond: BiggestInt # Bytes transferred in one second.
  73. lastProgressReport: float # Time
  74. toStore: string # Data left to upload (Only used with async)
  75. FtpClientObj* = FtpBaseObj[Socket]
  76. FtpClient* = ref FtpClientObj
  77. AsyncFtpClient* = ref AsyncFtpClientObj ## Async alternative to TFTPClient.
  78. AsyncFtpClientObj* = FtpBaseObj[asyncio.AsyncSocket]
  79. FTPEventType* = enum
  80. EvTransferProgress, EvLines, EvRetr, EvStore
  81. FTPEvent* = object ## Event
  82. filename*: string
  83. case typ*: FTPEventType
  84. of EvLines:
  85. lines*: string ## Lines that have been transferred.
  86. of EvRetr, EvStore: ## Retr/Store operation finished.
  87. nil
  88. of EvTransferProgress:
  89. bytesTotal*: BiggestInt ## Bytes total.
  90. bytesFinished*: BiggestInt ## Bytes transferred.
  91. speed*: BiggestInt ## Speed in bytes/s
  92. currentJob*: FTPJobType ## The current job being performed.
  93. ReplyError* = object of IOError
  94. FTPError* = object of IOError
  95. const multiLineLimit = 10000
  96. proc ftpClient*(address: string, port = Port(21),
  97. user, pass = ""): FtpClient =
  98. ## Create a ``FtpClient`` object.
  99. new(result)
  100. result.user = user
  101. result.pass = pass
  102. result.address = address
  103. result.port = port
  104. result.dsockConnected = false
  105. result.csock = socket()
  106. if result.csock == invalidSocket: raiseOSError(osLastError())
  107. template blockingOperation(sock: Socket, body: untyped) =
  108. body
  109. template blockingOperation(sock: asyncio.AsyncSocket, body: untyped) =
  110. sock.setBlocking(true)
  111. body
  112. sock.setBlocking(false)
  113. proc expectReply[T](ftp: FtpBase[T]): TaintedString =
  114. result = TaintedString""
  115. blockingOperation(ftp.csock):
  116. when T is Socket:
  117. ftp.csock.readLine(result)
  118. else:
  119. discard ftp.csock.readLine(result)
  120. var count = 0
  121. while result[3] == '-':
  122. ## Multi-line reply.
  123. var line = TaintedString""
  124. when T is Socket:
  125. ftp.csock.readLine(line)
  126. else:
  127. discard ftp.csock.readLine(line)
  128. result.add("\n" & line)
  129. count.inc()
  130. if count >= multiLineLimit:
  131. raise newException(ReplyError, "Reached maximum multi-line reply count.")
  132. proc send*[T](ftp: FtpBase[T], m: string): TaintedString =
  133. ## Send a message to the server, and wait for a primary reply.
  134. ## ``\c\L`` is added for you.
  135. ##
  136. ## **Note:** The server may return multiple lines of coded replies.
  137. blockingOperation(ftp.csock):
  138. ftp.csock.send(m & "\c\L")
  139. return ftp.expectReply()
  140. proc assertReply(received: TaintedString, expected: string) =
  141. if not received.string.startsWith(expected):
  142. raise newException(ReplyError,
  143. "Expected reply '$1' got: $2" % [
  144. expected, received.string])
  145. proc assertReply(received: TaintedString, expected: varargs[string]) =
  146. for i in items(expected):
  147. if received.string.startsWith(i): return
  148. raise newException(ReplyError,
  149. "Expected reply '$1' got: $2" %
  150. [expected.join("' or '"), received.string])
  151. proc createJob[T](ftp: FtpBase[T],
  152. prc: proc (ftp: FtpBase[T], async: bool): bool {.
  153. nimcall,gcsafe.},
  154. cmd: FTPJobType) =
  155. if ftp.jobInProgress:
  156. raise newException(FTPError, "Unable to do two jobs at once.")
  157. ftp.jobInProgress = true
  158. new(ftp.job)
  159. ftp.job.prc = prc
  160. ftp.job.typ = cmd
  161. case cmd
  162. of JRetrText:
  163. ftp.job.lines = ""
  164. of JRetr, JStore:
  165. ftp.job.toStore = ""
  166. proc deleteJob[T](ftp: FtpBase[T]) =
  167. assert ftp.jobInProgress
  168. ftp.jobInProgress = false
  169. case ftp.job.typ
  170. of JRetrText:
  171. ftp.job.lines = ""
  172. of JRetr, JStore:
  173. ftp.job.file.close()
  174. ftp.dsock.close()
  175. proc handleTask(s: AsyncSocket, ftp: AsyncFTPClient) =
  176. if ftp.jobInProgress:
  177. if ftp.job.typ in {JRetr, JStore}:
  178. if epochTime() - ftp.job.lastProgressReport >= 1.0:
  179. var r: FTPEvent
  180. ftp.job.lastProgressReport = epochTime()
  181. r.typ = EvTransferProgress
  182. r.bytesTotal = ftp.job.total
  183. r.bytesFinished = ftp.job.progress
  184. r.speed = ftp.job.oneSecond
  185. r.filename = ftp.job.filename
  186. r.currentJob = ftp.job.typ
  187. ftp.job.oneSecond = 0
  188. ftp.handleEvent(ftp, r)
  189. proc handleWrite(s: AsyncSocket, ftp: AsyncFTPClient) =
  190. if ftp.jobInProgress:
  191. if ftp.job.typ == JStore:
  192. assert (not ftp.job.prc(ftp, true))
  193. proc handleConnect(s: AsyncSocket, ftp: AsyncFTPClient) =
  194. ftp.dsockConnected = true
  195. assert(ftp.jobInProgress)
  196. if ftp.job.typ == JStore:
  197. s.setHandleWrite(proc (s: AsyncSocket) = handleWrite(s, ftp))
  198. else:
  199. s.delHandleWrite()
  200. proc handleRead(s: AsyncSocket, ftp: AsyncFTPClient) =
  201. assert ftp.jobInProgress
  202. assert ftp.job.typ != JStore
  203. # This can never return true, because it shouldn't check for code
  204. # 226 from csock.
  205. assert(not ftp.job.prc(ftp, true))
  206. proc pasv[T](ftp: FtpBase[T]) =
  207. ## Negotiate a data connection.
  208. when T is Socket:
  209. ftp.dsock = socket()
  210. if ftp.dsock == invalidSocket: raiseOSError(osLastError())
  211. elif T is AsyncSocket:
  212. ftp.dsock = asyncSocket()
  213. ftp.dsock.handleRead =
  214. proc (s: AsyncSocket) =
  215. handleRead(s, ftp)
  216. ftp.dsock.handleConnect =
  217. proc (s: AsyncSocket) =
  218. handleConnect(s, ftp)
  219. ftp.dsock.handleTask =
  220. proc (s: AsyncSocket) =
  221. handleTask(s, ftp)
  222. ftp.disp.register(ftp.dsock)
  223. else:
  224. {.fatal: "Incorrect socket instantiation".}
  225. var pasvMsg = ftp.send("PASV").string.strip.TaintedString
  226. assertReply(pasvMsg, "227")
  227. var betweenParens = captureBetween(pasvMsg.string, '(', ')')
  228. var nums = betweenParens.split(',')
  229. var ip = nums[0.. ^3]
  230. var port = nums[^2.. ^1]
  231. var properPort = port[0].parseInt()*256+port[1].parseInt()
  232. ftp.dsock.connect(ip.join("."), Port(properPort.toU16))
  233. when T is AsyncSocket:
  234. ftp.dsockConnected = false
  235. else:
  236. ftp.dsockConnected = true
  237. proc normalizePathSep(path: string): string =
  238. return replace(path, '\\', '/')
  239. proc connect*[T](ftp: FtpBase[T]) =
  240. ## Connect to the FTP server specified by ``ftp``.
  241. when T is AsyncSocket:
  242. blockingOperation(ftp.csock):
  243. ftp.csock.connect(ftp.address, ftp.port)
  244. elif T is Socket:
  245. ftp.csock.connect(ftp.address, ftp.port)
  246. else:
  247. {.fatal: "Incorrect socket instantiation".}
  248. var reply = ftp.expectReply()
  249. if reply.startsWith("120"):
  250. # 120 Service ready in nnn minutes.
  251. # We wait until we receive 220.
  252. reply = ftp.expectReply()
  253. # Handle 220 messages from the server
  254. assertReply ftp.expectReply(), "220"
  255. if ftp.user != "":
  256. assertReply(ftp.send("USER " & ftp.user), "230", "331")
  257. if ftp.pass != "":
  258. assertReply ftp.send("PASS " & ftp.pass), "230"
  259. proc pwd*[T](ftp: FtpBase[T]): string =
  260. ## Returns the current working directory.
  261. var wd = ftp.send("PWD")
  262. assertReply wd, "257"
  263. return wd.string.captureBetween('"') # "
  264. proc cd*[T](ftp: FtpBase[T], dir: string) =
  265. ## Changes the current directory on the remote FTP server to ``dir``.
  266. assertReply ftp.send("CWD " & dir.normalizePathSep), "250"
  267. proc cdup*[T](ftp: FtpBase[T]) =
  268. ## Changes the current directory to the parent of the current directory.
  269. assertReply ftp.send("CDUP"), "200"
  270. proc getLines[T](ftp: FtpBase[T], async: bool = false): bool =
  271. ## Downloads text data in ASCII mode
  272. ## Returns true if the download is complete.
  273. ## It doesn't if `async` is true, because it doesn't check for 226 then.
  274. if ftp.dsockConnected:
  275. var r = TaintedString""
  276. when T is AsyncSocket:
  277. if ftp.asyncDSock.readLine(r):
  278. if r.string == "":
  279. ftp.dsockConnected = false
  280. else:
  281. ftp.job.lines.add(r.string & "\n")
  282. elif T is Socket:
  283. assert(not async)
  284. ftp.dsock.readLine(r)
  285. if r.string == "":
  286. ftp.dsockConnected = false
  287. else:
  288. ftp.job.lines.add(r.string & "\n")
  289. else:
  290. {.fatal: "Incorrect socket instantiation".}
  291. if not async:
  292. var readSocks: seq[Socket] = @[ftp.csock]
  293. # This is only needed here. Asyncio gets this socket...
  294. blockingOperation(ftp.csock):
  295. if readSocks.select(1) != 0 and ftp.csock in readSocks:
  296. assertReply ftp.expectReply(), "226"
  297. return true
  298. proc listDirs*[T](ftp: FtpBase[T], dir: string = "",
  299. async = false): seq[string] =
  300. ## Returns a list of filenames in the given directory. If ``dir`` is "",
  301. ## the current directory is used. If ``async`` is true, this
  302. ## function will return immediately and it will be your job to
  303. ## use asyncio's ``poll`` to progress this operation.
  304. ftp.createJob(getLines[T], JRetrText)
  305. ftp.pasv()
  306. assertReply ftp.send("NLST " & dir.normalizePathSep), ["125", "150"]
  307. if not async:
  308. while not ftp.job.prc(ftp, false): discard
  309. result = splitLines(ftp.job.lines)
  310. ftp.deleteJob()
  311. else: return @[]
  312. proc fileExists*(ftp: FtpClient, file: string): bool {.deprecated.} =
  313. ## **Deprecated since version 0.9.0:** Please use ``existsFile``.
  314. ##
  315. ## Determines whether ``file`` exists.
  316. ##
  317. ## Warning: This function may block. Especially on directories with many
  318. ## files, because a full list of file names must be retrieved.
  319. var files = ftp.listDirs()
  320. for f in items(files):
  321. if f.normalizePathSep == file.normalizePathSep: return true
  322. proc existsFile*(ftp: FtpClient, file: string): bool =
  323. ## Determines whether ``file`` exists.
  324. ##
  325. ## Warning: This function may block. Especially on directories with many
  326. ## files, because a full list of file names must be retrieved.
  327. var files = ftp.listDirs()
  328. for f in items(files):
  329. if f.normalizePathSep == file.normalizePathSep: return true
  330. proc createDir*[T](ftp: FtpBase[T], dir: string, recursive: bool = false) =
  331. ## Creates a directory ``dir``. If ``recursive`` is true, the topmost
  332. ## subdirectory of ``dir`` will be created first, following the secondmost...
  333. ## etc. this allows you to give a full path as the ``dir`` without worrying
  334. ## about subdirectories not existing.
  335. if not recursive:
  336. assertReply ftp.send("MKD " & dir.normalizePathSep), "257"
  337. else:
  338. var reply = TaintedString""
  339. var previousDirs = ""
  340. for p in split(dir, {os.DirSep, os.AltSep}):
  341. if p != "":
  342. previousDirs.add(p)
  343. reply = ftp.send("MKD " & previousDirs)
  344. previousDirs.add('/')
  345. assertReply reply, "257"
  346. proc chmod*[T](ftp: FtpBase[T], path: string,
  347. permissions: set[FilePermission]) =
  348. ## Changes permission of ``path`` to ``permissions``.
  349. var userOctal = 0
  350. var groupOctal = 0
  351. var otherOctal = 0
  352. for i in items(permissions):
  353. case i
  354. of fpUserExec: userOctal.inc(1)
  355. of fpUserWrite: userOctal.inc(2)
  356. of fpUserRead: userOctal.inc(4)
  357. of fpGroupExec: groupOctal.inc(1)
  358. of fpGroupWrite: groupOctal.inc(2)
  359. of fpGroupRead: groupOctal.inc(4)
  360. of fpOthersExec: otherOctal.inc(1)
  361. of fpOthersWrite: otherOctal.inc(2)
  362. of fpOthersRead: otherOctal.inc(4)
  363. var perm = $userOctal & $groupOctal & $otherOctal
  364. assertReply ftp.send("SITE CHMOD " & perm &
  365. " " & path.normalizePathSep), "200"
  366. proc list*[T](ftp: FtpBase[T], dir: string = "", async = false): string =
  367. ## Lists all files in ``dir``. If ``dir`` is ``""``, uses the current
  368. ## working directory. If ``async`` is true, this function will return
  369. ## immediately and it will be your job to call asyncio's
  370. ## ``poll`` to progress this operation.
  371. ftp.createJob(getLines[T], JRetrText)
  372. ftp.pasv()
  373. assertReply(ftp.send("LIST" & " " & dir.normalizePathSep), ["125", "150"])
  374. if not async:
  375. while not ftp.job.prc(ftp, false): discard
  376. result = ftp.job.lines
  377. ftp.deleteJob()
  378. else:
  379. return ""
  380. proc retrText*[T](ftp: FtpBase[T], file: string, async = false): string =
  381. ## Retrieves ``file``. File must be ASCII text.
  382. ## If ``async`` is true, this function will return immediately and
  383. ## it will be your job to call asyncio's ``poll`` to progress this operation.
  384. ftp.createJob(getLines[T], JRetrText)
  385. ftp.pasv()
  386. assertReply ftp.send("RETR " & file.normalizePathSep), ["125", "150"]
  387. if not async:
  388. while not ftp.job.prc(ftp, false): discard
  389. result = ftp.job.lines
  390. ftp.deleteJob()
  391. else:
  392. return ""
  393. proc getFile[T](ftp: FtpBase[T], async = false): bool =
  394. if ftp.dsockConnected:
  395. var r = "".TaintedString
  396. var bytesRead = 0
  397. var returned = false
  398. if async:
  399. when T is Socket:
  400. raise newException(FTPError, "FTPClient must be async.")
  401. else:
  402. bytesRead = ftp.dsock.recvAsync(r, BufferSize)
  403. returned = bytesRead != -1
  404. else:
  405. bytesRead = ftp.dsock.recv(r, BufferSize)
  406. returned = true
  407. let r2 = r.string
  408. if r2 != "":
  409. ftp.job.progress.inc(r2.len)
  410. ftp.job.oneSecond.inc(r2.len)
  411. ftp.job.file.write(r2)
  412. elif returned and r2 == "":
  413. ftp.dsockConnected = false
  414. when T is Socket:
  415. if not async:
  416. var readSocks: seq[Socket] = @[ftp.csock]
  417. blockingOperation(ftp.csock):
  418. if readSocks.select(1) != 0 and ftp.csock in readSocks:
  419. assertReply ftp.expectReply(), "226"
  420. return true
  421. proc retrFile*[T](ftp: FtpBase[T], file, dest: string, async = false) =
  422. ## Downloads ``file`` and saves it to ``dest``. Usage of this function
  423. ## asynchronously is recommended to view the progress of the download.
  424. ## The ``EvRetr`` event is passed to the specified ``handleEvent`` function
  425. ## when the download is finished, and the ``filename`` field will be equal
  426. ## to ``file``.
  427. ftp.createJob(getFile[T], JRetr)
  428. ftp.job.file = open(dest, mode = fmWrite)
  429. ftp.pasv()
  430. var reply = ftp.send("RETR " & file.normalizePathSep)
  431. assertReply reply, ["125", "150"]
  432. if {'(', ')'} notin reply.string:
  433. raise newException(ReplyError, "Reply has no file size.")
  434. var fileSize: BiggestInt
  435. if reply.string.captureBetween('(', ')').parseBiggestInt(fileSize) == 0:
  436. raise newException(ReplyError, "Reply has no file size.")
  437. ftp.job.total = fileSize
  438. ftp.job.lastProgressReport = epochTime()
  439. ftp.job.filename = file.normalizePathSep
  440. if not async:
  441. while not ftp.job.prc(ftp, false): discard
  442. ftp.deleteJob()
  443. proc doUpload[T](ftp: FtpBase[T], async = false): bool =
  444. if ftp.dsockConnected:
  445. if ftp.job.toStore.len() > 0:
  446. assert(async)
  447. let bytesSent = ftp.dsock.sendAsync(ftp.job.toStore)
  448. if bytesSent == ftp.job.toStore.len:
  449. ftp.job.toStore = ""
  450. elif bytesSent != ftp.job.toStore.len and bytesSent != 0:
  451. ftp.job.toStore = ftp.job.toStore[bytesSent .. ^1]
  452. ftp.job.progress.inc(bytesSent)
  453. ftp.job.oneSecond.inc(bytesSent)
  454. else:
  455. var s = newStringOfCap(4000)
  456. var len = ftp.job.file.readBuffer(addr(s[0]), 4000)
  457. setLen(s, len)
  458. if len == 0:
  459. # File finished uploading.
  460. ftp.dsock.close()
  461. ftp.dsockConnected = false
  462. if not async:
  463. assertReply ftp.expectReply(), "226"
  464. return true
  465. return false
  466. if not async:
  467. ftp.dsock.send(s)
  468. else:
  469. let bytesSent = ftp.dsock.sendAsync(s)
  470. if bytesSent == 0:
  471. ftp.job.toStore.add(s)
  472. elif bytesSent != s.len:
  473. ftp.job.toStore.add(s[bytesSent .. ^1])
  474. len = bytesSent
  475. ftp.job.progress.inc(len)
  476. ftp.job.oneSecond.inc(len)
  477. proc store*[T](ftp: FtpBase[T], file, dest: string, async = false) =
  478. ## Uploads ``file`` to ``dest`` on the remote FTP server. Usage of this
  479. ## function asynchronously is recommended to view the progress of
  480. ## the download.
  481. ## The ``EvStore`` event is passed to the specified ``handleEvent`` function
  482. ## when the upload is finished, and the ``filename`` field will be
  483. ## equal to ``file``.
  484. ftp.createJob(doUpload[T], JStore)
  485. ftp.job.file = open(file)
  486. ftp.job.total = ftp.job.file.getFileSize()
  487. ftp.job.lastProgressReport = epochTime()
  488. ftp.job.filename = file
  489. ftp.pasv()
  490. assertReply ftp.send("STOR " & dest.normalizePathSep), ["125", "150"]
  491. if not async:
  492. while not ftp.job.prc(ftp, false): discard
  493. ftp.deleteJob()
  494. proc close*[T](ftp: FtpBase[T]) =
  495. ## Terminates the connection to the server.
  496. assertReply ftp.send("QUIT"), "221"
  497. if ftp.jobInProgress: ftp.deleteJob()
  498. ftp.csock.close()
  499. ftp.dsock.close()
  500. proc csockHandleRead(s: AsyncSocket, ftp: AsyncFTPClient) =
  501. if ftp.jobInProgress:
  502. assertReply ftp.expectReply(), "226" # Make sure the transfer completed.
  503. var r: FTPEvent
  504. case ftp.job.typ
  505. of JRetrText:
  506. r.typ = EvLines
  507. r.lines = ftp.job.lines
  508. of JRetr:
  509. r.typ = EvRetr
  510. r.filename = ftp.job.filename
  511. if ftp.job.progress != ftp.job.total:
  512. raise newException(FTPError, "Didn't download full file.")
  513. of JStore:
  514. r.typ = EvStore
  515. r.filename = ftp.job.filename
  516. if ftp.job.progress != ftp.job.total:
  517. raise newException(FTPError, "Didn't upload full file.")
  518. ftp.deleteJob()
  519. ftp.handleEvent(ftp, r)
  520. proc asyncFTPClient*(address: string, port = Port(21),
  521. user, pass = "",
  522. handleEvent: proc (ftp: AsyncFTPClient, ev: FTPEvent) {.closure,gcsafe.} =
  523. (proc (ftp: AsyncFTPClient, ev: FTPEvent) = discard)): AsyncFTPClient =
  524. ## Create a ``AsyncFTPClient`` object.
  525. ##
  526. ## Use this if you want to use asyncio's dispatcher.
  527. var dres: AsyncFtpClient
  528. new(dres)
  529. dres.user = user
  530. dres.pass = pass
  531. dres.address = address
  532. dres.port = port
  533. dres.dsockConnected = false
  534. dres.handleEvent = handleEvent
  535. dres.csock = asyncSocket()
  536. dres.csock.handleRead =
  537. proc (s: AsyncSocket) =
  538. csockHandleRead(s, dres)
  539. result = dres
  540. proc register*(d: Dispatcher, ftp: AsyncFTPClient): Delegate {.discardable.} =
  541. ## Registers ``ftp`` with dispatcher ``d``.
  542. ftp.disp = d
  543. return ftp.disp.register(ftp.csock)
  544. when not defined(testing) and isMainModule:
  545. proc main =
  546. var d = newDispatcher()
  547. let hev =
  548. proc (ftp: AsyncFTPClient, event: FTPEvent) =
  549. case event.typ
  550. of EvStore:
  551. echo("Upload finished!")
  552. ftp.retrFile("payload.jpg", "payload2.jpg", async = true)
  553. of EvTransferProgress:
  554. var time: int64 = -1
  555. if event.speed != 0:
  556. time = (event.bytesTotal - event.bytesFinished) div event.speed
  557. echo(event.currentJob)
  558. echo(event.speed div 1000, " kb/s. - ",
  559. event.bytesFinished, "/", event.bytesTotal,
  560. " - ", time, " seconds")
  561. echo(d.len)
  562. of EvRetr:
  563. echo("Download finished!")
  564. ftp.close()
  565. echo d.len
  566. else: assert(false)
  567. var ftp = asyncFTPClient("example.com", user = "foo", pass = "bar", handleEvent = hev)
  568. d.register(ftp)
  569. d.len.echo()
  570. ftp.connect()
  571. echo "connected"
  572. ftp.store("payload.jpg", "payload.jpg", async = true)
  573. d.len.echo()
  574. echo "uploading..."
  575. while true:
  576. if not d.poll(): break
  577. main()
  578. when not defined(testing) and isMainModule:
  579. var ftp = ftpClient("example.com", user = "foo", pass = "bar")
  580. ftp.connect()
  581. echo ftp.pwd()
  582. echo ftp.list()
  583. echo("uploading")
  584. ftp.store("payload.jpg", "payload.jpg", async = false)
  585. echo("Upload complete")
  586. ftp.retrFile("payload.jpg", "payload2.jpg", async = false)
  587. echo("Download complete")
  588. sleep(5000)
  589. ftp.close()
  590. sleep(200)