diff options
Diffstat (limited to 'lib/pure/ftpclient.nim')
-rw-r--r-- | lib/pure/ftpclient.nim | 237 |
1 files changed, 120 insertions, 117 deletions
diff --git a/lib/pure/ftpclient.nim b/lib/pure/ftpclient.nim index 5a8cf50b2..eb62354d8 100644 --- a/lib/pure/ftpclient.nim +++ b/lib/pure/ftpclient.nim @@ -10,6 +10,10 @@ include "system/inclrtl" import sockets, strutils, parseutils, times, os, asyncio +from asyncnet import nil +from rawsockets import nil +from asyncdispatch import PFuture + ## This module **partially** implements an FTP client as specified ## by `RFC 959 <http://tools.ietf.org/html/rfc959>`_. ## @@ -33,34 +37,32 @@ import sockets, strutils, parseutils, times, os, asyncio ## to change. type - FTPClientObj* = object of RootObj - case isAsync: bool - of false: - csock: Socket # Command connection socket - dsock: Socket # Data connection socket - else: - dummyA, dummyB: pointer # workaround a Nim API issue - asyncCSock: AsyncSocket - asyncDSock: AsyncSocket + FtpBase*[SockType] = ref FtpBaseObj[SockType] + FtpBaseObj*[SockType] = object + csock*: SockType + dsock*: SockType + when SockType is asyncio.AsyncSocket: handleEvent*: proc (ftp: AsyncFTPClient, ev: FTPEvent){.closure,gcsafe.} disp: Dispatcher asyncDSockID: Delegate - user, pass: string - address: string - port: Port + user*, pass*: string + address*: string + when SockType is asyncnet.AsyncSocket: + port*: rawsockets.Port + else: + port*: Port - jobInProgress: bool - job: ref FTPJob - - dsockConnected: bool + jobInProgress*: bool + job*: FTPJob[SockType] - FTPClient* = ref FTPClientObj + dsockConnected*: bool FTPJobType* = enum JRetrText, JRetr, JStore - FTPJob = object - prc: proc (ftp: PFTPClient, async: bool): bool {.nimcall, gcsafe.} + FtpJob[T] = ref FtpJobObj[T] + FTPJobObj[T] = object + prc: proc (ftp: FTPBase[T], async: bool): bool {.nimcall, gcsafe.} case typ*: FTPJobType of JRetrText: lines: string @@ -74,8 +76,11 @@ type toStore: string # Data left to upload (Only used with async) else: nil - AsyncFTPClient* = ref AsyncFTPClientObj ## Async alternative to TFTPClient. - AsyncFTPClientObj* = object of FTPClientObj + FtpClientObj* = FtpBaseObj[Socket] + FtpClient* = ref FtpClientObj + + AsyncFtpClient* = ref AsyncFtpClientObj ## Async alternative to TFTPClient. + AsyncFtpClientObj* = FtpBaseObj[asyncio.AsyncSocket] FTPEventType* = enum EvTransferProgress, EvLines, EvRetr, EvStore @@ -103,38 +108,38 @@ type ].} proc ftpClient*(address: string, port = TPort(21), - user, pass = ""): PFTPClient = - ## Create a ``PFTPClient`` object. + user, pass = ""): FtpClient = + ## Create a ``FtpClient`` object. new(result) result.user = user result.pass = pass result.address = address result.port = port - result.isAsync = false result.dsockConnected = false result.csock = socket() - if result.csock == InvalidSocket: osError(osLastError()) + if result.csock == invalidSocket: raiseOSError(osLastError()) -proc getDSock(ftp: PFTPClient): TSocket = - if ftp.isAsync: return ftp.asyncDSock else: return ftp.dsock +proc getDSock[T](ftp: FtpBase[T]): TSocket = + return ftp.dsock -proc getCSock(ftp: PFTPClient): TSocket = - if ftp.isAsync: return ftp.asyncCSock else: return ftp.csock +proc getCSock[T](ftp: FtpBase[T]): TSocket = + return ftp.csock template blockingOperation(sock: TSocket, body: stmt) {.immediate.} = - if ftp.isAsync: - sock.setBlocking(true) body - if ftp.isAsync: - sock.setBlocking(false) -proc expectReply(ftp: PFTPClient): TaintedString = +template blockingOperation(sock: asyncio.PAsyncSocket, body: stmt) {.immediate.} = + sock.setBlocking(true) + body + sock.setBlocking(false) + +proc expectReply[T](ftp: FtpBase[T]): TaintedString = result = TaintedString"" blockingOperation(ftp.getCSock()): ftp.getCSock().readLine(result) -proc send*(ftp: PFTPClient, m: string): TaintedString = +proc send*[T](ftp: FtpBase[T], m: string): TaintedString = ## Send a message to the server, and wait for a primary reply. ## ``\c\L`` is added for you. blockingOperation(ftp.getCSock()): @@ -154,8 +159,8 @@ proc assertReply(received: TaintedString, expected: varargs[string]) = "Expected reply '$1' got: $2" % [expected.join("' or '"), received.string]) -proc createJob(ftp: PFTPClient, - prc: proc (ftp: PFTPClient, async: bool): bool {. +proc createJob[T](ftp: FtpBase[T], + prc: proc (ftp: FtpBase[T], async: bool): bool {. nimcall,gcsafe.}, cmd: FTPJobType) = if ftp.jobInProgress: @@ -170,7 +175,7 @@ proc createJob(ftp: PFTPClient, of JRetr, JStore: ftp.job.toStore = "" -proc deleteJob(ftp: PFTPClient) = +proc deleteJob[T](ftp: FtpBase[T]) = assert ftp.jobInProgress ftp.jobInProgress = false case ftp.job.typ @@ -178,12 +183,9 @@ proc deleteJob(ftp: PFTPClient) = ftp.job.lines = "" of JRetr, JStore: ftp.job.file.close() - if ftp.isAsync: - ftp.asyncDSock.close() - else: - ftp.dsock.close() + ftp.dsock.close() -proc handleTask(s: PAsyncSocket, ftp: PFTPClient) = +proc handleTask(s: PAsyncSocket, ftp: PAsyncFTPClient) = if ftp.jobInProgress: if ftp.job.typ in {JRetr, JStore}: if epochTime() - ftp.job.lastProgressReport >= 1.0: @@ -198,12 +200,12 @@ proc handleTask(s: PAsyncSocket, ftp: PFTPClient) = ftp.job.oneSecond = 0 ftp.handleEvent(PAsyncFTPClient(ftp), r) -proc handleWrite(s: PAsyncSocket, ftp: PFTPClient) = +proc handleWrite(s: PAsyncSocket, ftp: PAsyncFTPClient) = if ftp.jobInProgress: if ftp.job.typ == JStore: assert (not ftp.job.prc(ftp, true)) -proc handleConnect(s: PAsyncSocket, ftp: PFTPClient) = +proc handleConnect(s: PAsyncSocket, ftp: PAsyncFTPClient) = ftp.dsockConnected = true assert(ftp.jobInProgress) if ftp.job.typ == JStore: @@ -211,30 +213,32 @@ proc handleConnect(s: PAsyncSocket, ftp: PFTPClient) = else: s.delHandleWrite() -proc handleRead(s: PAsyncSocket, ftp: PFTPClient) = +proc handleRead(s: PAsyncSocket, ftp: PAsyncFTPClient) = assert ftp.jobInProgress assert ftp.job.typ != JStore # This can never return true, because it shouldn't check for code # 226 from csock. assert(not ftp.job.prc(ftp, true)) -proc pasv(ftp: PFTPClient) = +proc pasv[T](ftp: FtpBase[T]) = ## Negotiate a data connection. - if not ftp.isAsync: + when T is TSocket: ftp.dsock = socket() - if ftp.dsock == InvalidSocket: osError(osLastError()) - else: - ftp.asyncDSock = AsyncSocket() - ftp.asyncDSock.handleRead = + if ftp.dsock == invalidSocket: raiseOSError(osLastError()) + elif T is PAsyncSocket: + ftp.dsock = asyncSocket() + ftp.dsock.handleRead = proc (s: PAsyncSocket) = handleRead(s, ftp) - ftp.asyncDSock.handleConnect = + ftp.dsock.handleConnect = proc (s: PAsyncSocket) = handleConnect(s, ftp) - ftp.asyncDSock.handleTask = + ftp.dsock.handleTask = proc (s: PAsyncSocket) = handleTask(s, ftp) - ftp.disp.register(ftp.asyncDSock) + ftp.disp.register(ftp.dsock) + else: + {.fatal: "Incorrect socket instantiation".} var pasvMsg = ftp.send("PASV").string.strip.TaintedString assertReply(pasvMsg, "227") @@ -243,23 +247,24 @@ proc pasv(ftp: PFTPClient) = var ip = nums[0.. -3] var port = nums[-2.. -1] var properPort = port[0].parseInt()*256+port[1].parseInt() - if ftp.isAsync: - ftp.asyncDSock.connect(ip.join("."), TPort(properPort.toU16)) - ftp.dsockConnected = False + ftp.dsock.connect(ip.join("."), TPort(properPort.toU16)) + when T is PAsyncSocket: + ftp.dsockConnected = false else: - ftp.dsock.connect(ip.join("."), TPort(properPort.toU16)) - ftp.dsockConnected = True + ftp.dsockConnected = true proc normalizePathSep(path: string): string = return replace(path, '\\', '/') -proc connect*(ftp: PFTPClient) = +proc connect*[T](ftp: FtpBase[T]) = ## Connect to the FTP server specified by ``ftp``. - if ftp.isAsync: - blockingOperation(ftp.asyncCSock): - ftp.asyncCSock.connect(ftp.address, ftp.port) - else: + when T is PAsyncSocket: + blockingOperation(ftp.csock): + ftp.csock.connect(ftp.address, ftp.port) + elif T is TSocket: ftp.csock.connect(ftp.address, ftp.port) + else: + {.fatal: "Incorrect socket instantiation".} # TODO: Handle 120? or let user handle it. assertReply ftp.expectReply(), "220" @@ -270,39 +275,41 @@ proc connect*(ftp: PFTPClient) = if ftp.pass != "": assertReply ftp.send("PASS " & ftp.pass), "230" -proc pwd*(ftp: PFTPClient): string = +proc pwd*(ftp: FtpClient): string = ## Returns the current working directory. var wd = ftp.send("PWD") assertReply wd, "257" return wd.string.captureBetween('"') # " -proc cd*(ftp: PFTPClient, dir: string) = +proc cd*(ftp: FtpClient, dir: string) = ## Changes the current directory on the remote FTP server to ``dir``. assertReply ftp.send("CWD " & dir.normalizePathSep), "250" -proc cdup*(ftp: PFTPClient) = +proc cdup*(ftp: FtpClient) = ## Changes the current directory to the parent of the current directory. assertReply ftp.send("CDUP"), "200" -proc getLines(ftp: PFTPClient, async: bool = false): bool = +proc getLines[T](ftp: FtpBase[T], async: bool = false): bool = ## Downloads text data in ASCII mode ## Returns true if the download is complete. ## It doesn't if `async` is true, because it doesn't check for 226 then. if ftp.dsockConnected: var r = TaintedString"" - if ftp.isAsync: + when T is PAsyncSocket: if ftp.asyncDSock.readLine(r): if r.string == "": ftp.dsockConnected = false else: ftp.job.lines.add(r.string & "\n") - else: + elif T is TSocket: assert(not async) ftp.dsock.readLine(r) if r.string == "": ftp.dsockConnected = false else: ftp.job.lines.add(r.string & "\n") + else: + {.fatal: "Incorrect socket instantiation".} if not async: var readSocks: seq[TSocket] = @[ftp.getCSock()] @@ -312,14 +319,14 @@ proc getLines(ftp: PFTPClient, async: bool = false): bool = assertReply ftp.expectReply(), "226" return true -proc listDirs*(ftp: PFTPClient, dir: string = "", +proc listDirs*[T](ftp: FtpBase[T], dir: string = "", async = false): seq[string] = ## Returns a list of filenames in the given directory. If ``dir`` is "", ## the current directory is used. If ``async`` is true, this ## function will return immediately and it will be your job to ## use asyncio's ``poll`` to progress this operation. - ftp.createJob(getLines, JRetrText) + ftp.createJob(getLines[T], JRetrText) ftp.pasv() assertReply ftp.send("NLST " & dir.normalizePathSep), ["125", "150"] @@ -330,7 +337,7 @@ proc listDirs*(ftp: PFTPClient, dir: string = "", ftp.deleteJob() else: return @[] -proc fileExists*(ftp: PFTPClient, file: string): bool {.deprecated.} = +proc fileExists*(ftp: FtpClient, file: string): bool {.deprecated.} = ## **Deprecated since version 0.9.0:** Please use ``existsFile``. ## ## Determines whether ``file`` exists. @@ -341,7 +348,7 @@ proc fileExists*(ftp: PFTPClient, file: string): bool {.deprecated.} = for f in items(files): if f.normalizePathSep == file.normalizePathSep: return true -proc existsFile*(ftp: PFTPClient, file: string): bool = +proc existsFile*(ftp: FtpClient, file: string): bool = ## Determines whether ``file`` exists. ## ## Warning: This function may block. Especially on directories with many @@ -350,7 +357,7 @@ proc existsFile*(ftp: PFTPClient, file: string): bool = for f in items(files): if f.normalizePathSep == file.normalizePathSep: return true -proc createDir*(ftp: PFTPClient, dir: string, recursive: bool = false) = +proc createDir*(ftp: FtpClient, dir: string, recursive: bool = false) = ## Creates a directory ``dir``. If ``recursive`` is true, the topmost ## subdirectory of ``dir`` will be created first, following the secondmost... ## etc. this allows you to give a full path as the ``dir`` without worrying @@ -360,14 +367,14 @@ proc createDir*(ftp: PFTPClient, dir: string, recursive: bool = false) = else: var reply = TaintedString"" var previousDirs = "" - for p in split(dir, {os.dirSep, os.altSep}): + for p in split(dir, {os.DirSep, os.AltSep}): if p != "": previousDirs.add(p) reply = ftp.send("MKD " & previousDirs) previousDirs.add('/') assertReply reply, "257" -proc chmod*(ftp: PFTPClient, path: string, +proc chmod*(ftp: FtpClient, path: string, permissions: set[TFilePermission]) = ## Changes permission of ``path`` to ``permissions``. var userOctal = 0 @@ -389,12 +396,12 @@ proc chmod*(ftp: PFTPClient, path: string, assertReply ftp.send("SITE CHMOD " & perm & " " & path.normalizePathSep), "200" -proc list*(ftp: PFTPClient, dir: string = "", async = false): string = +proc list*[T](ftp: FtpBase[T], dir: string = "", async = false): string = ## Lists all files in ``dir``. If ``dir`` is ``""``, uses the current ## working directory. If ``async`` is true, this function will return ## immediately and it will be your job to call asyncio's ## ``poll`` to progress this operation. - ftp.createJob(getLines, JRetrText) + ftp.createJob(getLines[T], JRetrText) ftp.pasv() assertReply(ftp.send("LIST" & " " & dir.normalizePathSep), ["125", "150"]) @@ -406,11 +413,11 @@ proc list*(ftp: PFTPClient, dir: string = "", async = false): string = else: return "" -proc retrText*(ftp: PFTPClient, file: string, async = false): string = +proc retrText*[T](ftp: FtpBase[T], file: string, async = false): string = ## Retrieves ``file``. File must be ASCII text. ## If ``async`` is true, this function will return immediately and ## it will be your job to call asyncio's ``poll`` to progress this operation. - ftp.createJob(getLines, JRetrText) + ftp.createJob(getLines[T], JRetrText) ftp.pasv() assertReply ftp.send("RETR " & file.normalizePathSep), ["125", "150"] @@ -421,15 +428,17 @@ proc retrText*(ftp: PFTPClient, file: string, async = false): string = else: return "" -proc getFile(ftp: PFTPClient, async = false): bool = +proc getFile[T](ftp: FtpBase[T], async = false): bool = if ftp.dsockConnected: var r = "".TaintedString var bytesRead = 0 var returned = false if async: - if not ftp.isAsync: raise newException(EFTP, "FTPClient must be async.") - bytesRead = ftp.AsyncDSock.recvAsync(r, BufferSize) - returned = bytesRead != -1 + when T is TSocket: + raise newException(EFTP, "FTPClient must be async.") + else: + bytesRead = ftp.dsock.recvAsync(r, BufferSize) + returned = bytesRead != -1 else: bytesRead = getDSock(ftp).recv(r, BufferSize) returned = true @@ -439,7 +448,7 @@ proc getFile(ftp: PFTPClient, async = false): bool = ftp.job.oneSecond.inc(r2.len) ftp.job.file.write(r2) elif returned and r2 == "": - ftp.dsockConnected = False + ftp.dsockConnected = false if not async: var readSocks: seq[TSocket] = @[ftp.getCSock()] @@ -448,20 +457,20 @@ proc getFile(ftp: PFTPClient, async = false): bool = assertReply ftp.expectReply(), "226" return true -proc retrFile*(ftp: PFTPClient, file, dest: string, async = false) = +proc retrFile*[T](ftp: FtpBase[T], file, dest: string, async = false) = ## Downloads ``file`` and saves it to ``dest``. Usage of this function ## asynchronously is recommended to view the progress of the download. ## The ``EvRetr`` event is passed to the specified ``handleEvent`` function ## when the download is finished, and the ``filename`` field will be equal ## to ``file``. - ftp.createJob(getFile, JRetr) + ftp.createJob(getFile[T], JRetr) ftp.job.file = open(dest, mode = fmWrite) ftp.pasv() var reply = ftp.send("RETR " & file.normalizePathSep) assertReply reply, ["125", "150"] if {'(', ')'} notin reply.string: raise newException(EInvalidReply, "Reply has no file size.") - var fileSize: biggestInt + var fileSize: BiggestInt if reply.string.captureBetween('(', ')').parseBiggestInt(fileSize) == 0: raise newException(EInvalidReply, "Reply has no file size.") @@ -473,11 +482,11 @@ proc retrFile*(ftp: PFTPClient, file, dest: string, async = false) = while not ftp.job.prc(ftp, false): discard ftp.deleteJob() -proc doUpload(ftp: PFTPClient, async = false): bool = +proc doUpload[T](ftp: FtpBase[T], async = false): bool = if ftp.dsockConnected: if ftp.job.toStore.len() > 0: assert(async) - let bytesSent = ftp.asyncDSock.sendAsync(ftp.job.toStore) + let bytesSent = ftp.dsock.sendAsync(ftp.job.toStore) if bytesSent == ftp.job.toStore.len: ftp.job.toStore = "" elif bytesSent != ftp.job.toStore.len and bytesSent != 0: @@ -490,7 +499,7 @@ proc doUpload(ftp: PFTPClient, async = false): bool = setLen(s, len) if len == 0: # File finished uploading. - if ftp.isAsync: ftp.asyncDSock.close() else: ftp.dsock.close() + ftp.dsock.close() ftp.dsockConnected = false if not async: @@ -501,7 +510,7 @@ proc doUpload(ftp: PFTPClient, async = false): bool = if not async: getDSock(ftp).send(s) else: - let bytesSent = ftp.asyncDSock.sendAsync(s) + let bytesSent = ftp.dsock.sendAsync(s) if bytesSent == 0: ftp.job.toStore.add(s) elif bytesSent != s.len: @@ -511,14 +520,14 @@ proc doUpload(ftp: PFTPClient, async = false): bool = ftp.job.progress.inc(len) ftp.job.oneSecond.inc(len) -proc store*(ftp: PFTPClient, file, dest: string, async = false) = +proc store*[T](ftp: FtpBase[T], file, dest: string, async = false) = ## Uploads ``file`` to ``dest`` on the remote FTP server. Usage of this ## function asynchronously is recommended to view the progress of ## the download. ## The ``EvStore`` event is passed to the specified ``handleEvent`` function ## when the upload is finished, and the ``filename`` field will be ## equal to ``file``. - ftp.createJob(doUpload, JStore) + ftp.createJob(doUpload[T], JStore) ftp.job.file = open(file) ftp.job.total = ftp.job.file.getFileSize() ftp.job.lastProgressReport = epochTime() @@ -531,16 +540,12 @@ proc store*(ftp: PFTPClient, file, dest: string, async = false) = while not ftp.job.prc(ftp, false): discard ftp.deleteJob() -proc close*(ftp: PFTPClient) = +proc close*[T](ftp: FtpBase[T]) = ## Terminates the connection to the server. assertReply ftp.send("QUIT"), "221" if ftp.jobInProgress: ftp.deleteJob() - if ftp.isAsync: - ftp.asyncCSock.close() - ftp.asyncDSock.close() - else: - ftp.csock.close() - ftp.dsock.close() + ftp.csock.close() + ftp.dsock.close() proc csockHandleRead(s: PAsyncSocket, ftp: PAsyncFTPClient) = if ftp.jobInProgress: @@ -571,36 +576,34 @@ proc asyncFTPClient*(address: string, port = TPort(21), ## Create a ``PAsyncFTPClient`` object. ## ## Use this if you want to use asyncio's dispatcher. - var dres: PAsyncFTPClient + var dres: AsyncFtpClient new(dres) dres.user = user dres.pass = pass dres.address = address dres.port = port - dres.isAsync = true dres.dsockConnected = false dres.handleEvent = handleEvent - dres.asyncCSock = AsyncSocket() - dres.asyncCSock.handleRead = - proc (s: PAsyncSocket) = + dres.csock = asyncSocket() + dres.csock.handleRead = + proc (s: AsyncSocket) = csockHandleRead(s, dres) result = dres proc register*(d: PDispatcher, ftp: PAsyncFTPClient): PDelegate {.discardable.} = ## Registers ``ftp`` with dispatcher ``d``. - assert ftp.isAsync ftp.disp = d - return ftp.disp.register(ftp.asyncCSock) + return ftp.disp.register(ftp.csock) when isMainModule: proc main = var d = newDispatcher() let hev = - proc (ftp: PAsyncFTPClient, event: TFTPEvent) = + proc (ftp: AsyncFTPClient, event: FTPEvent) = case event.typ of EvStore: echo("Upload finished!") - ftp.retrFile("payload.JPG", "payload2.JPG", async = true) + ftp.retrFile("payload.jpg", "payload2.jpg", async = true) of EvTransferProgress: var time: int64 = -1 if event.speed != 0: @@ -615,13 +618,13 @@ when isMainModule: ftp.close() echo d.len else: assert(false) - var ftp = asyncFTPClient("picheta.me", user = "test", pass = "asf", handleEvent = hev) + var ftp = asyncFTPClient("example.com", user = "foo", pass = "bar", handleEvent = hev) d.register(ftp) d.len.echo() ftp.connect() echo "connected" - ftp.store("payload.JPG", "payload.JPG", async = true) + ftp.store("payload.jpg", "payload.jpg", async = true) d.len.echo() echo "uploading..." while true: @@ -629,15 +632,15 @@ when isMainModule: main() when isMainModule and false: - var ftp = ftpClient("picheta.me", user = "asdasd", pass = "asfwq") + var ftp = ftpClient("example.com", user = "foo", pass = "bar") ftp.connect() echo ftp.pwd() echo ftp.list() echo("uploading") - ftp.store("payload.JPG", "payload.JPG", async = false) + ftp.store("payload.jpg", "payload.jpg", async = false) echo("Upload complete") - ftp.retrFile("payload.JPG", "payload2.JPG", async = false) + ftp.retrFile("payload.jpg", "payload2.jpg", async = false) echo("Download complete") sleep(5000) |