+ (log-debug "Cache file not found for URI: ~a" (url->string uri))
+ #f)))
+
+(: str->url (-> String (Option String)))
+(define (str->url s)
+ (with-handlers*
+ ([exn:fail? (λ (e) #f)])
+ (string->url s)))
+
+(: str->peer (String (Option Peer)))
+(define (str->peer str)
+ (log-debug "Parsing peer string: ~v" str)
+ (match
+ (regexp-match
+ #px"(([^\\s\t]+)[\\s\t]+)?([a-zA-Z]+://[^\\s\t]*)[\\s\t]*(#\\s*(.*))?"
+ str)
+ [(list _wholething
+ _nick-with-space
+ nick
+ url
+ _comment-with-hash
+ comment)
+ (match (str->url url)
+ [#f
+ (log-error "Invalid URI in peer string: ~v" str)
+ #f]
+ [url (Peer nick url comment)])]
+ [_
+ (log-debug "Invalid peer string: ~v" str)
+ #f]))
+
+(module+ test
+ (check-equal?
+ (str->peer "foo http://bar/file.txt # some rando")
+ (Peer "foo" (str->url "http://bar/file.txt") "some rando"))
+ (check-equal?
+ (str->peer "http://bar/file.txt # some rando")
+ (Peer #f (str->url "http://bar/file.txt") "some rando"))
+ (check-equal?
+ (str->peer "http://bar/file.txt #")
+ (Peer #f (str->url "http://bar/file.txt") ""))
+ (check-equal?
+ (str->peer "http://bar/file.txt#") ; XXX URLs can have #s
+ (Peer #f (str->url "http://bar/file.txt#") #f))
+ (check-equal?
+ (str->peer "http://bar/file.txt")
+ (Peer #f (str->url "http://bar/file.txt") #f))
+ (check-equal?
+ (str->peer "foo http://bar/file.txt")
+ (Peer "foo" (str->url "http://bar/file.txt") #f))
+ (check-equal?
+ (str->peer "foo bar # baz")
+ #f)
+ (check-equal?
+ (str->peer "foo bar://baz # quux")
+ (Peer "foo" (str->url "bar://baz") "quux"))
+ (check-equal?
+ (str->peer "foo bar//baz # quux")
+ #f))
+
+(: filter-comments (-> (Listof String) (Listof String)))
+(define (filter-comments lines)
+ (filter-not (λ (line) (string-prefix? line "#")) lines))
+
+(: str->peers (-> String (Listof Peer)))
+(define (str->peers str)
+ (filter-map str->peer (filter-comments (str->lines str))))
+
+(: peers->file (-> (Listof Peers) Path-String Void))
+(define (peers->file peers path)
+ (display-lines-to-file
+ (map (match-lambda
+ [(Peer n u c)
+ (format "~a~a~a"
+ (if n (format "~a " n) "")
+ (url->string u)
+ (if c (format " # ~a" c) ""))])
+ peers)
+ path
+ #:exists 'replace))
+
+(: file->peers (-> Path-String (Listof Peer)))
+(define (file->peers file-path)
+ (if (file-exists? file-path)
+ (str->peers (file->string file-path))
+ (begin
+ (log-warning "File does not exist: ~v" (path->string file-path))
+ '())))
+
+(define re-rfc2822
+ #px"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-9]{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) ([0-2][0-9]):([0-6][0-9]):([0-6][0-9]) GMT")
+
+(: b->n (-> Bytes (Option Number)))
+(define (b->n b)
+ (string->number (bytes->string/utf-8 b)))
+
+(: mon->num (-> Bytes Natural))
+(define/match (mon->num mon)
+ [(#"Jan") 1]
+ [(#"Feb") 2]
+ [(#"Mar") 3]
+ [(#"Apr") 4]
+ [(#"May") 5]
+ [(#"Jun") 6]
+ [(#"Jul") 7]
+ [(#"Aug") 8]
+ [(#"Sep") 9]
+ [(#"Oct") 10]
+ [(#"Nov") 11]
+ [(#"Dec") 12])
+
+(: rfc2822->epoch (-> Bytes (Option Nonnegative-Integer)))
+(define (rfc2822->epoch timestamp)
+ (match (regexp-match re-rfc2822 timestamp)
+ [(list _ _ dd mo yyyy HH MM SS)
+ #:when (and dd mo yyyy HH MM SS)
+ (find-seconds (b->n SS)
+ (b->n MM)
+ (b->n HH)
+ (b->n dd)
+ (mon->num mo)
+ (b->n yyyy)
+ #f)]
+ [_
+ #f]))
+
+(: user-agent String)
+(define user-agent
+ (let*
+ ([prog-name "tt"]
+ [prog-version (info:#%info-lookup 'version)]
+ [prog-uri "https://github.com/xandkar/tt"]
+ [user-peer-file (build-path tt-home-dir "me")]
+ [user
+ (if (file-exists? user-peer-file)
+ (match (first (file->peers user-peer-file))
+ [(Peer #f u _) (format "+~a" (url->string u) )]
+ [(Peer n u _) (format "+~a; @~a" (url->string u) n)])
+ (format "+~a" prog-uri))])
+ (format "~a/~a (~a)" prog-name prog-version user)))
+
+(: header-get (-> (Listof Bytes) Bytes (Option Bytes)))
+(define (header-get headers name)
+ (match (filter-map (curry extract-field name) headers)
+ [(list val) val]
+ [_ #f]))
+
+(: uri-download-from-port
+ (-> Url (Listof (U Bytes String)) Input-Port
+ (U 'skipped-cached 'downloaded-new))) ; TODO 'ok|'error ?
+(define (uri-download-from-port u headers body-input)
+ (define u-str (url->string u))
+ (log-debug "uri-download-from-port ~v into ~v" u-str cached-object-path)
+ (define cached-object-path (url->cache-object-path u))
+ (define cached-etag-path (url->cache-etag-path u))
+ (define cached-lmod-path (url->cache-lmod-path u))
+ (define etag (header-get headers #"ETag"))
+ (define lmod (header-get headers #"Last-Modified"))
+ (define lmod-curr (if lmod (rfc2822->epoch lmod) #f))
+ (define lmod-prev (if (file-exists? cached-lmod-path)
+ (rfc2822->epoch (file->bytes cached-lmod-path))
+ #f))
+ (log-debug "lmod-curr:~v lmod-prev:~v" lmod-curr lmod-prev)
+ (define cached?
+ (or (and etag
+ (file-exists? cached-etag-path)
+ (bytes=? etag (file->bytes cached-etag-path))
+ (begin
+ (log-debug "ETags match, skipping the rest of ~v" u-str)
+ #t))
+ (and lmod-curr
+ lmod-prev
+ (<= lmod-curr lmod-prev)
+ (begin
+ (log-debug "Last-Modified <= current skipping the rest of ~v" u-str)
+ #t))))
+ (if (not cached?)