#lang racket
(provide start-listen finish-transfer kill-transfer wait-transfer send-file filetransfer?)
(require racket/tcp)
(require racket/async-channel)
(require file/sha1)
(require srfi/13)
(require srfi/14)
(require (planet williams/packed-binary/packed-binary))
(require (planet erast/file-utils))
(define (start-listen local-port save-path from-ip progress-proc final-proc
[timeout 60.0] [file-table (make-hash)] [listen-timeout 604800.0] )
(define custodian (make-custodian))
(define finalize-channel (make-async-channel))
(define listener (tcp-listen local-port 1 #t #f))
(define (accept-connection/maybe listener)
(with-handlers ([exn:fail:network? (lambda (exn) #f)])
(let ((ports (sync/timeout/enable-break listen-timeout (tcp-accept-evt listener))))
(if (or (not from-ip) (string=? (let-values (((in out) (tcp-addresses (first ports) #f))) in)
from-ip))
ports
#f))))
(make-filetransfer
listener
(thread
(lambda ()
(progress-proc 'listening save-path 0)
(let loop ((in-out-ports (accept-connection/maybe listener)))
(when in-out-ports
(receive-file (first in-out-ports) (second in-out-ports) save-path progress-proc final-proc timeout file-table #:channel finalize-channel)
(unless (thread-try-receive)
(loop (accept-connection/maybe listener)))))))
custodian
finalize-channel))
(define (receive-file in-port out-port local-path progress-proc final-proc [timeout 60.0] [file-table (make-hash)] #:channel finalize-channel)
(progress-proc 'preparing local-path 0) (define checksum (sync/network-timeout timeout (read-bytes-evt 20 in-port))) (define name-len (car (unpack "<H" (sync/network-timeout timeout (read-bytes-evt 2 in-port))))) (define name (sync/network-timeout timeout (read-bytes-evt name-len in-port))) (define key (bytes-append checksum name)) (define path (hash-ref file-table key #f))
(cond
((and path (file-exists? path)) (write-packed "<q" out-port (file-size path))
(flush-output out-port)
(read-raw-file in-port path progress-proc final-proc timeout finalize-channel)
path)
(else (let ((local-name (build-path local-path (sanitize (bytes->string/utf-8 name) local-path))))
(hash-set! file-table key local-name)
(write-packed "<q" out-port 0)
(flush-output out-port)
(read-raw-file in-port local-name progress-proc final-proc timeout finalize-channel)
local-name))))
(define (percent n m)
(ceiling (* (/ n m) 100)))
(define (kill-transfer t)
(when (filetransfer-listener t)
(tcp-close (filetransfer-listener t)))
(custodian-shutdown-all (filetransfer-custodian t)))
(define (finish-transfer t [timeout 259200.0])
(thread-send (filetransfer-thread t) #t)
(wait-transfer t timeout)
(kill-transfer t))
(define (wait-transfer t [timeout 259200.0])
(sync/timeout timeout (filetransfer-finalized? t)))
(define (send-file remote-hostname remote-port path suggested-filename progress-proc final-proc [timeout 60.0])
(define custodian (make-custodian))
(define finalize-channel (make-async-channel))
(progress-proc 'connecting path 0)
(let-values (((in-port out-port) (tcp-connect/enable-break remote-hostname remote-port)))
(make-filetransfer #f
(thread
(lambda () (send-file-to-port suggested-filename path out-port in-port progress-proc final-proc timeout #:channel finalize-channel)))
custodian
finalize-channel)))
(define (send-file-to-port suggested-name file-path out-port in-port progress-proc final-proc [timeout 60.0] #:channel finalize-channel)
(progress-proc 'preparing file-path 0)
(define file-name-bytes (string->bytes/utf-8 suggested-name))
(write-bytes (checksum-file file-path) out-port)
(write-packed "<H" out-port (bytes-length file-name-bytes))
(write-bytes file-name-bytes out-port)
(flush-output out-port)
(define offset (car (unpack "<q" (sync/network-timeout timeout (read-bytes-evt 8 in-port)))))
(define size (file-size file-path))
(if (> offset 0)
(progress-proc 'resuming file-path 0)
(progress-proc 'starting file-path 0))
(write-packed "<q" out-port (- size offset))
(define time_0 (current-milliseconds))
(when (< offset size)
(write-file file-path out-port offset size progress-proc final-proc timeout))
(flush-output out-port)
(final-proc 'finished file-path (- (current-milliseconds) time_0) size)
(async-channel-put finalize-channel #t))
(define-struct filetransfer (listener thread custodian finalized?))
(define-struct (exn:fail:network:timeout
exn:fail:network)
(a-srcloc)
#:property prop:exn:srclocs
(lambda (a-struct)
(match a-struct
[(struct exn:fail:network:timeout
(msg marks a-srcloc))
(list a-srcloc)])))
(define-syntax sync/network-timeout
(syntax-rules ()
((_ timeout expr ...) (let ((result (sync/timeout timeout expr ...)))
(if result
result
(raise exn:fail:network:timeout #t))))))
(define (time)
(current-milliseconds))
(define (read-raw-file in-port path progress-proc final-proc timeout finalize-channel)
(define time_0 (time))
(define buffsize 16384)
(define buffer (make-bytes buffsize))
(define size (car (unpack "<q" (sync/network-timeout timeout (read-bytes-evt 8 in-port)))))
(define out-port (open-output-file path #:mode 'binary #:exists 'append))
(when (> size 0)
(let loop ((total-read 0))
(cond
((> (+ total-read buffsize) size) (read-bytes! buffer in-port 0 (- size total-read))
(write-bytes buffer out-port 0 (- size total-read))
(progress-proc 'receiving path
(percent size size)))
(else (read-bytes! buffer in-port)
(when (> (percent total-read size) (percent (- total-read buffsize) size))
(progress-proc 'receiving path
(percent total-read size)))
(write-bytes buffer out-port)
(loop (+ total-read buffsize))))))
(close-output-port out-port)
(final-proc 'finished path (- (time) time_0) size)
(async-channel-put finalize-channel #t))
(define char-set:allowed-in-file-name (char-set-union char-set:letter+digit (char-set #\_ #\- #\.)))
(define (sanitize file-name local-path)
(let ((name (string-filter char-set:allowed-in-file-name file-name)))
(if (string=? (filename-main name) "")
(if (string=? (filename-suffix name) "")
(make-unique-name (number->string (current-milliseconds)) local-path)
(make-unique-name (string-append (number->string (current-milliseconds)) "." (filename-suffix name)) local-path))
(make-unique-name name local-path))))
(define (checksum-file path)
(let ((in-port (open-input-file path #:mode 'binary))
(buff (make-bytes 16384))
(size (file-size path)))
(file-position in-port (max 0 (- (quotient (file-size path) 2) 8192)))
(read-bytes-avail!* buff in-port)
(close-input-port in-port)
(sha1-bytes (open-input-bytes buff))))
(define (write-file path out-port offset size progress-proc final-proc timeout)
(define in-port (open-input-file path #:mode 'binary))
(define buffsize 16384)
(define buffer (make-bytes buffsize))
(let loop ((total-read 0))
(when (> (percent total-read size) (percent (- total-read buffsize) size))
(progress-proc 'sending path (percent total-read size)))
(cond
((> (+ total-read buffsize) size) (read-bytes! buffer in-port 0 (- size total-read))
(write-bytes buffer out-port 0 (- size total-read))
(progress-proc 'sending path (percent size size)))
(else (read-bytes! buffer in-port)
(write-bytes buffer out-port)
(loop (+ total-read buffsize)))))
(close-input-port in-port))