How to make a file upload form and make the server handle it

How to make a file upload form and make the server handle it.
I know how to create a upload form https://docs.racket-lang.org/web-server/formlets.html#%28def._%28%28lib._web-server%2Fformlets%2Finput..rkt%29._file-upload%29%29
But how to handle it?
And also need a hint about how to design a file download function

2 Likes

Here's a toy example. It does make me think that send/formlet and embed/formlet should get an optional #:attributes argument, and I want to check if the web server internally understands filename*.

#lang web-server/insta
;; SPDX-License-Identifier: CC0-1.0

(require web-server/formlets)

(define title "File Echo Demo")

(define (start req)
  (define file
    (send/formlet*
     (formlet (#%# (p ,{=> (file-upload #:attributes '([required ""]))
                           file})
                   (p (input ([type "submit"]
                              [value "Upload"]))))
              file)
     (λ (k-url formlet-xs)
       (make-page
        "Upload a File"
        `[(p "Give me a file, and I will give it back to you.")
          (form ([method "POST"]
                 [enctype "multipart/form-data"]
                 [action ,k-url])
                ,@formlet-xs)]))))
  (redirect/get)
  (match-define (binding:file id filename-bytes headers content-bytes) file)
  (define filename
    (bytes->string/utf-8 filename-bytes))
  (send/suspend
   (λ (k-url)
     (make-page
      "Here is your file."
      `[(p (a ([href ,k-url]
               [download ,filename])
              "Click here to download it."))
        (p "I hope it used UTF-8!")
        (h3 ,filename)
        ;; x-expressions save us from injection attacks :)
        (pre ,(bytes->string/utf-8 content-bytes))])))
  (response/output
   #:mime-type (cond
                 [(headers-assq* #"Content-Type" headers)
                  => header-value]
                 [else
                  #f])
   #:headers (list (header #"Content-Disposition"
                           (bytes-append #"attachment; filename=\""
                                         filename-bytes
                                         ;; we assume it's correctly encoded,
                                         ;; since we got it from a header,
                                         ;; but see aslo filename* and RFC 5987
                                         #"\"")))
   (λ (out)
     (write-bytes content-bytes out))))

(define (send/formlet* formlet wrap)
  ;; unfortunately, send/formlet and embed/formlet don't
  ;; have a way to set enctype="multipart/form-data"
  (formlet-process 
   formlet
   (send/suspend
    (lambda (k-url)
      (wrap k-url (formlet-display formlet))))))

(define (make-page subtitle body-xs)
  (response/xexpr
   #:preamble #"<!DOCTYPE html>"
   `(html ([lang "en"])
          (head (title ,subtitle " | " ,title)
                (meta ([charset "utf-8"]))
                (meta ([name "viewport"]
                       [content "width=device-width, initial-scale=1.0"])))
          (body
           (h1 ,title)
           (h2 ,subtitle)
           ,@body-xs))))

Edit: Fixed code formatting.

I just copy and run it, but when I test it, something is wrong.

Exception
The application raised an exception with the message:

bytes->string/utf-8: byte string is not a well-formed UTF-8 encoding
  byte string: #"PK\3\4\24\0\6\0\b\0\0\0!\0\337\244\322lZ\1\0\0 \5\0\0\23\0\b\2[Content_Types].xml \242\4\2(\240\0\2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0...
Stack trace:

<unknown procedure> at:
  line 29, column 3, in file 3-unsaved-editor
<unknown procedure> at:
  line 375, column 33, in file C:\Program Files\Racket\collects\racket\contract\private\arrow-higher-order.rkt
<unknown procedure> at:
  line 89, column 5, in file C:\Program Files\Racket\share\pkgs\web-server-lib\web-server\servlet\web.rkt
send/suspend at:
  line 85, column 0, in file C:\Program Files\Racket\share\pkgs\web-server-lib\web-server\servlet\web.rkt
<unknown procedure> at:
  line 486, column 18, in file C:\Program Files\Racket\collects\racket\contract\private\arrow-val-first.rkt
start at:
  line 8, column 0, in file 3-unsaved-editor
<unknown procedure> at:
  line 375, column 33, in file C:\Program Files\Racket\collects\racket\contract\private\arrow-higher-order.rkt
<unknown procedure> at:
  line 375, column 33, in file C:\Program Files\Racket\collects\racket\contract\private\arrow-higher-order.rkt
<unknown procedure> at:
  line 375, column 33, in file C:\Program Files\Racket\collects\racket\contract\private\arrow-higher-order.rkt
<unknown procedure> at:
  line 375, column 33, in file C:\Program Files\Racket\collects\racket\contract\private\arrow-higher-order.rkt
<unknown procedure> at:
  line 63, column 2, in file C:\Program Files\Racket\share\pkgs\web-server-lib\web-server\dispatchers\dispatch-servlets.rkt
select-handler/no-breaks at:
  line 163, column 2, in file C:\Program Files\Racket\collects\racket\private\more-scheme.rkt
<unknown procedure> at:
  line 101, column 2, in file C:\Program Files\Racket\share\pkgs\web-server-lib\web-server\private\dispatch-server-with-connect-unit.rkt

What was the filename of the file you uploaded? Were the file's name and contents
in UTF-8? What happens if you try uploading a different file—say, the .rkt file
for the program itself?

When it's a .txt file it's okay but when it come to binary file or .docx there's something wrong.
I guess is the bytes->string/utf-8 problem

Yes, that was a shortcut for the demo. Here's a variant that works with arbitrary files:

#lang web-server/insta
;; SPDX-License-Identifier: CC0-1.0

(require web-server/formlets
         net/base64)

(define title "File Echo Demo")

(define (start req)
  (define file
    (send/formlet*
     (formlet (#%# (p ,{=> (file-upload #:attributes '([required ""]))
                           file})
                   (p (input ([type "submit"]
                              [value "Upload"]))))
              file)
     (λ (k-url formlet-xs)
       (make-page
        "Upload a File"
        `[(p "Give me a file, and I will give it back to you.")
          (form ([method "POST"]
                 [enctype "multipart/form-data"]
                 [action ,k-url])
                ,@formlet-xs)]))))
  (redirect/get)
  (match-define (binding:file id filename-bytes headers content-bytes) file)
  (define filename
    (bytes->string/utf-8 filename-bytes))
  (send/suspend
   (λ (k-url)
     (make-page
      "Here is your file."
      `[(p (a ([href ,k-url]
               [download ,filename])
              "Click here to download it."))
        (h3 ,filename)
        ;; x-expressions save us from injection attacks :)
        ,(or (with-handlers ([exn:fail? (λ (e) #f)])
               `(pre ,(bytes->string/utf-8 content-bytes)))
             `(div (p (i "Base64-encoded"))
                   (pre ,(bytes->string/utf-8
                          (base64-encode content-bytes #"\n")))))])))
  (response/output
   #:mime-type (cond
                 [(headers-assq* #"Content-Type" headers)
                  => header-value]
                 [else
                  #f])
   #:headers (list (header #"Content-Disposition"
                           (bytes-append #"attachment; filename=\""
                                         filename-bytes
                                         ;; we assume it's correctly encoded,
                                         ;; since we got it from a header,
                                         ;; but see aslo filename* and RFC 5987
                                         #"\"")))
   (λ (out)
     (write-bytes content-bytes out))))

(define (send/formlet* formlet wrap)
  ;; unfortunately, send/formlet and embed/formlet don't
  ;; have a way to set enctype="multipart/form-data"
  (formlet-process 
   formlet
   (send/suspend
    (lambda (k-url)
      (wrap k-url (formlet-display formlet))))))

(define (make-page subtitle body-xs)
  (response/xexpr
   #:preamble #"<!DOCTYPE html>"
   `(html ([lang "en"])
          (head (title ,subtitle " | " ,title)
                (meta ([charset "utf-8"]))
                (meta ([name "viewport"]
                       [content "width=device-width, initial-scale=1.0"])))
          (body
           (h1 ,title)
           (h2 ,subtitle)
           ,@body-xs))))

May I ask what this form is for specifically?

The program seems to work properly after it is commented out.

This implements the Post–Redirect–Get pattern to prevent duplicate form submission in the presence of the "Back" button etc. In this toy example, duplicate submission isn't really a problem, but it's generally the right practice.

1 Like