upload.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. // Package upload provides functions for dealing with uploading files in a fast and safe way
  2. package upload
  3. import (
  4. "bytes"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/textproto"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. log "github.com/sirupsen/logrus"
  13. "github.com/xyproto/algernon/utils"
  14. lua "github.com/xyproto/gopher-lua"
  15. )
  16. // For dealing with uploaded files in POST method handlers
  17. const (
  18. // Class is an identifier for the UploadedFile class in Lua
  19. Class = "UploadedFile"
  20. // Upload limit, in bytes
  21. defaultUploadLimit int64 = 32 * utils.MiB
  22. // Memory usage while uploading
  23. defaultMemoryLimit int64 = 32 * utils.MiB
  24. // Chunk size when reading uploaded file
  25. chunkSize int64 = 4 * utils.KiB
  26. // chunkSize = defaultMemoryLimit
  27. )
  28. // UploadedFile represents a file that has been uploaded but not yet been
  29. // written to file.
  30. type UploadedFile struct {
  31. req *http.Request
  32. header textproto.MIMEHeader
  33. buf *bytes.Buffer
  34. scriptdir string
  35. filename string
  36. }
  37. // New creates a struct that is used for accepting an uploaded file
  38. //
  39. // The client will send all the data, if the data is over the given size,
  40. // if the Content-Length is wrongly set to a value below the the uploadLimit.
  41. // However, the buffer and memory usage will not grow despite this.
  42. //
  43. // uploadLimit is in bytes.
  44. //
  45. // Note that the client may appear to keep sending the file even when the
  46. // server has stopped receiving it, for files that are too large.
  47. func New(req *http.Request, scriptdir, formID string, uploadLimit int64) (*UploadedFile, error) {
  48. clientLengthTotal, err := strconv.Atoi(req.Header.Get("Content-Length"))
  49. if err != nil {
  50. log.Error("Invalid Content-Length: ", req.Header.Get("Content-Length"))
  51. }
  52. // Remove the extra 20 bytes and convert to int64
  53. clientLength := int64(clientLengthTotal - 20)
  54. if clientLength > uploadLimit {
  55. return nil, fmt.Errorf("uploaded file was too large: %s according to Content-Length (current limit is %s)", utils.DescribeBytes(clientLength), utils.DescribeBytes(uploadLimit))
  56. }
  57. // For specifying the memory usage when uploading
  58. if errMem := req.ParseMultipartForm(defaultMemoryLimit); errMem != nil {
  59. return nil, errMem
  60. }
  61. file, handler, err := req.FormFile(formID)
  62. if err != nil {
  63. return nil, err
  64. }
  65. defer file.Close()
  66. // Store the data in a buffer, for later usage.
  67. buf := new(bytes.Buffer)
  68. // Read the data in chunks
  69. var totalWritten, writtenBytes, i int64
  70. for i = 0; i < int64(uploadLimit); i += chunkSize {
  71. writtenBytes, err = io.CopyN(buf, file, chunkSize)
  72. totalWritten += writtenBytes
  73. if totalWritten > uploadLimit {
  74. // File too large
  75. return nil, fmt.Errorf("uploaded file was too large: %d bytes (limit is %d bytes)", totalWritten, uploadLimit)
  76. } else if writtenBytes < chunkSize || err == io.EOF {
  77. // Done writing
  78. break
  79. } else if err != nil {
  80. // Error when copying data
  81. return nil, err
  82. }
  83. }
  84. // all ok
  85. return &UploadedFile{req, handler.Header, buf, scriptdir, handler.Filename}, nil
  86. }
  87. // Get the first argument, "self", and cast it from userdata to
  88. // an UploadedFile, which contains the file data and information.
  89. func checkUploadedFile(L *lua.LState) *UploadedFile {
  90. ud := L.CheckUserData(1)
  91. if uploadedfile, ok := ud.Value.(*UploadedFile); ok {
  92. return uploadedfile
  93. }
  94. L.ArgError(1, "UploadedFile expected")
  95. return nil
  96. }
  97. // Create a new Upload file
  98. func constructUploadedFile(L *lua.LState, req *http.Request, scriptdir, formID string, uploadLimit int64) (*lua.LUserData, error) {
  99. // Create a new UploadedFile
  100. uploadedfile, err := New(req, scriptdir, formID, uploadLimit)
  101. if err != nil {
  102. return nil, err
  103. }
  104. // Create a new userdata struct
  105. ud := L.NewUserData()
  106. ud.Value = uploadedfile
  107. L.SetMetatable(ud, L.GetTypeMetatable(Class))
  108. return ud, nil
  109. }
  110. // String representation
  111. func uploadedfileToString(L *lua.LState) int {
  112. L.Push(lua.LString("Uploaded file"))
  113. return 1 // number of results
  114. }
  115. // File name
  116. func uploadedfileName(L *lua.LState) int {
  117. ulf := checkUploadedFile(L) // arg 1
  118. L.Push(lua.LString(ulf.filename))
  119. return 1 // number of results
  120. }
  121. // File size
  122. func uploadedfileSize(L *lua.LState) int {
  123. ulf := checkUploadedFile(L) // arg 1
  124. L.Push(lua.LNumber(ulf.buf.Len()))
  125. return 1 // number of results
  126. }
  127. // Mime type
  128. func uploadedfileMimeType(L *lua.LState) int {
  129. ulf := checkUploadedFile(L) // arg 1
  130. contentType := ""
  131. if contentTypes, ok := ulf.header["Content-Type"]; ok {
  132. if len(contentTypes) > 0 {
  133. contentType = contentTypes[0]
  134. }
  135. }
  136. L.Push(lua.LString(contentType))
  137. return 1 // number of results
  138. }
  139. // Write the uploaded file to the given full filename.
  140. // Does not overwrite files.
  141. func (ulf *UploadedFile) write(fullFilename string, fperm os.FileMode) error {
  142. // Check if the file already exists
  143. if _, err := os.Stat(fullFilename); err == nil { // exists
  144. log.Error(fullFilename, " already exists")
  145. return fmt.Errorf("File exists: " + fullFilename)
  146. }
  147. // Write the uploaded file
  148. f, err := os.OpenFile(fullFilename, os.O_WRONLY|os.O_CREATE, fperm)
  149. if err != nil {
  150. log.Error("Error when creating ", fullFilename)
  151. return err
  152. }
  153. defer f.Close()
  154. // Copy the data to a new buffer, to keep the data and the length
  155. fileDataBuffer := bytes.NewBuffer(ulf.buf.Bytes())
  156. if _, err := io.Copy(f, fileDataBuffer); err != nil {
  157. log.Error("Error when writing: " + err.Error())
  158. return err
  159. }
  160. return nil
  161. }
  162. // Save the file locally
  163. func uploadedfileSave(L *lua.LState) int {
  164. ulf := checkUploadedFile(L) // arg 1
  165. givenFilename := ""
  166. if L.GetTop() == 2 {
  167. givenFilename = L.ToString(2) // optional argument
  168. }
  169. // optional argument, file permissions
  170. var givenPermissions os.FileMode = 0o660
  171. if L.GetTop() == 3 {
  172. givenPermissions = os.FileMode(L.ToInt(3))
  173. }
  174. // Use the given filename instead of the default one, if given
  175. var filename string
  176. if givenFilename != "" {
  177. filename = givenFilename
  178. } else {
  179. filename = ulf.filename
  180. }
  181. // Get the full path
  182. writeFilename := filepath.Join(ulf.scriptdir, filename)
  183. // Write the file and return true if successful
  184. L.Push(lua.LBool(ulf.write(writeFilename, givenPermissions) == nil))
  185. return 1 // number of results
  186. }
  187. // Save the file locally, to a given directory
  188. func uploadedfileSaveIn(L *lua.LState) int {
  189. ulf := checkUploadedFile(L) // arg 1
  190. givenDirectory := L.ToString(2) // required argument
  191. // optional argument, file permissions
  192. var givenPermissions os.FileMode = 0o660
  193. if L.GetTop() == 3 {
  194. givenPermissions = os.FileMode(L.ToInt(3))
  195. }
  196. // Get the full path
  197. var writeFilename string
  198. if filepath.IsAbs(givenDirectory) {
  199. writeFilename = filepath.Join(givenDirectory, ulf.filename)
  200. } else {
  201. writeFilename = filepath.Join(ulf.scriptdir, givenDirectory, ulf.filename)
  202. }
  203. // Write the file and return true if successful
  204. L.Push(lua.LBool(ulf.write(writeFilename, givenPermissions) == nil))
  205. return 1 // number of results
  206. }
  207. // Retrieve the file content
  208. func uploadedfileGet(L *lua.LState) int {
  209. ulf := checkUploadedFile(L) // arg 1
  210. L.Push(lua.LString(ulf.buf.Bytes()))
  211. return 1 // number of results
  212. }
  213. // The hash map methods that are to be registered
  214. var uploadedfileMethods = map[string]lua.LGFunction{
  215. "__tostring": uploadedfileToString,
  216. "filename": uploadedfileName,
  217. "size": uploadedfileSize,
  218. "mimetype": uploadedfileMimeType,
  219. "save": uploadedfileSave,
  220. "savein": uploadedfileSaveIn,
  221. "content": uploadedfileGet,
  222. }
  223. // Load makes functions related to saving an uploaded file available
  224. func Load(L *lua.LState, w http.ResponseWriter, req *http.Request, scriptdir string) {
  225. // Register the UploadedFile class and the methods that belongs with it.
  226. mt := L.NewTypeMetatable(Class)
  227. mt.RawSetH(lua.LString("__index"), mt)
  228. L.SetFuncs(mt, uploadedfileMethods)
  229. // The constructor for the UploadedFile userdata
  230. // Takes a form ID (string) and an optional file upload limit in MiB
  231. // (number). Returns the userdata and an empty string on success.
  232. // Returns nil and an error message on failure.
  233. L.SetGlobal("UploadedFile", L.NewFunction(func(L *lua.LState) int {
  234. formID := L.ToString(1)
  235. if formID == "" {
  236. L.ArgError(1, "form ID expected")
  237. }
  238. uploadLimit := defaultUploadLimit
  239. if L.GetTop() == 2 {
  240. uploadLimit = int64(L.ToInt(2)) * utils.MiB // optional upload limit, in MiB
  241. }
  242. // Construct a new UploadedFile
  243. userdata, err := constructUploadedFile(L, req, scriptdir, formID, uploadLimit)
  244. if err != nil {
  245. // Log the error
  246. log.Error(err)
  247. // Return an invalid UploadedFile object and an error string.
  248. // It's up to the Lua script to send an error to the client.
  249. L.Push(lua.LNil)
  250. L.Push(lua.LString(err.Error()))
  251. return 2 // Number of returned values
  252. }
  253. // Return the Lua UploadedFile object and an empty error string
  254. L.Push(userdata)
  255. L.Push(lua.LString(""))
  256. return 2 // Number of returned values
  257. }))
  258. }