mediaServer.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. package main
  2. import (
  3. "crypto/md5"
  4. "encoding/hex"
  5. "errors"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "imuslab.com/arozos/mod/common"
  15. "imuslab.com/arozos/mod/filesystem"
  16. fs "imuslab.com/arozos/mod/filesystem"
  17. "imuslab.com/arozos/mod/network/gzipmiddleware"
  18. )
  19. /*
  20. Media Server
  21. This function serve large file objects like video and audio file via asynchronize go routine :)
  22. Example usage:
  23. /media/?file=user:/Desktop/test/02.Orchestra- エミール (Addendum version).mp3
  24. /media/?file=user:/Desktop/test/02.Orchestra- エミール (Addendum version).mp3&download=true
  25. This will serve / download the file located at files/users/{username}/Desktop/test/02.Orchestra- エミール (Addendum version).mp3
  26. PLEASE ALWAYS USE URLENCODE IN THE LINK PASSED INTO THE /media ENDPOINT
  27. */
  28. func mediaServer_init() {
  29. if *enable_gzip {
  30. http.HandleFunc("/media/", gzipmiddleware.CompressFunc(serverMedia))
  31. http.HandleFunc("/media/getMime/", gzipmiddleware.CompressFunc(serveMediaMime))
  32. } else {
  33. http.HandleFunc("/media/", serverMedia)
  34. http.HandleFunc("/media/getMime/", serveMediaMime)
  35. }
  36. //Download API always bypass gzip no matter if gzip mode is enabled
  37. http.HandleFunc("/media/download/", serverMedia)
  38. }
  39. //This function validate the incoming media request and return fsh, vpath, rpath and err if any
  40. func media_server_validateSourceFile(w http.ResponseWriter, r *http.Request) (*filesystem.FileSystemHandler, string, string, error) {
  41. username, err := authAgent.GetUserName(w, r)
  42. if err != nil {
  43. return nil, "", "", errors.New("User not logged in")
  44. }
  45. userinfo, _ := userHandler.GetUserInfoFromUsername(username)
  46. //Validate url valid
  47. if strings.Count(r.URL.String(), "?") > 1 {
  48. return nil, "", "", errors.New("Invalid paramters. Multiple ? found")
  49. }
  50. targetfile, _ := common.Mv(r, "file", false)
  51. targetfile, err = url.QueryUnescape(targetfile)
  52. if err != nil {
  53. return nil, "", "", err
  54. }
  55. if targetfile == "" {
  56. return nil, "", "", errors.New("Missing paramter 'file'")
  57. }
  58. //Translate the virtual directory to realpath
  59. fsh, subpath, err := GetFSHandlerSubpathFromVpath(targetfile)
  60. if err != nil {
  61. return nil, "", "", errors.New("Unable to load from target file system")
  62. }
  63. fshAbs := fsh.FileSystemAbstraction
  64. realFilepath, err := fshAbs.VirtualPathToRealPath(subpath, userinfo.Username)
  65. if fshAbs.FileExists(realFilepath) && fshAbs.IsDir(realFilepath) {
  66. return nil, "", "", errors.New("Given path is not a file")
  67. }
  68. if err != nil {
  69. return nil, "", "", errors.New("Unable to translate the given filepath")
  70. }
  71. if !fshAbs.FileExists(realFilepath) {
  72. //Sometime if url is not URL encoded, this error might be shown as well
  73. //Try to use manual segmentation
  74. originalURL := r.URL.String()
  75. //Must be pre-processed with system special URI Decode function to handle edge cases
  76. originalURL = fs.DecodeURI(originalURL)
  77. if strings.Contains(originalURL, "&download=true") {
  78. originalURL = strings.ReplaceAll(originalURL, "&download=true", "")
  79. } else if strings.Contains(originalURL, "download=true") {
  80. originalURL = strings.ReplaceAll(originalURL, "download=true", "")
  81. }
  82. if strings.Contains(originalURL, "&file=") {
  83. originalURL = strings.ReplaceAll(originalURL, "&file=", "file=")
  84. }
  85. urlInfo := strings.Split(originalURL, "file=")
  86. possibleVirtualFilePath := urlInfo[len(urlInfo)-1]
  87. possibleRealpath, err := fshAbs.VirtualPathToRealPath(possibleVirtualFilePath, userinfo.Username)
  88. if err != nil {
  89. systemWideLogger.PrintAndLog("Media Server", "Error when trying to serve file in compatibility mode", err)
  90. return nil, "", "", errors.New("Error when trying to serve file in compatibility mode")
  91. }
  92. if fshAbs.FileExists(possibleRealpath) {
  93. realFilepath = possibleRealpath
  94. systemWideLogger.PrintAndLog("Media Server", "Serving file "+filepath.Base(possibleRealpath)+" in compatibility mode. Do not to use '&' or '+' sign in filename! ", nil)
  95. return fsh, targetfile, realFilepath, nil
  96. } else {
  97. return nil, "", "", errors.New("File not exists")
  98. }
  99. }
  100. return fsh, targetfile, realFilepath, nil
  101. }
  102. func serveMediaMime(w http.ResponseWriter, r *http.Request) {
  103. targetFsh, _, realFilepath, err := media_server_validateSourceFile(w, r)
  104. if err != nil {
  105. common.SendErrorResponse(w, err.Error())
  106. return
  107. }
  108. targetFshAbs := targetFsh.FileSystemAbstraction
  109. if targetFsh.RequireBuffer {
  110. //File is not on local. Guess its mime by extension
  111. common.SendTextResponse(w, "application/"+filepath.Ext(realFilepath)[1:])
  112. return
  113. }
  114. mime := "text/directory"
  115. if !targetFshAbs.IsDir(realFilepath) {
  116. m, _, err := fs.GetMime(realFilepath)
  117. if err != nil {
  118. mime = ""
  119. }
  120. mime = m
  121. }
  122. common.SendTextResponse(w, mime)
  123. }
  124. func serverMedia(w http.ResponseWriter, r *http.Request) {
  125. userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
  126. //Serve normal media files
  127. targetFsh, vpath, realFilepath, err := media_server_validateSourceFile(w, r)
  128. if err != nil {
  129. common.SendErrorResponse(w, err.Error())
  130. return
  131. }
  132. targetFshAbs := targetFsh.FileSystemAbstraction
  133. //Check if downloadMode
  134. downloadMode := false
  135. dw, _ := common.Mv(r, "download", false)
  136. if dw == "true" {
  137. downloadMode = true
  138. }
  139. //New download implementations, allow /download to be used instead of &download=true
  140. if strings.Contains(r.RequestURI, "media/download/?file=") {
  141. downloadMode = true
  142. }
  143. //Serve the file
  144. if downloadMode {
  145. escapedRealFilepath, err := url.PathUnescape(realFilepath)
  146. if err != nil {
  147. common.SendErrorResponse(w, err.Error())
  148. return
  149. }
  150. filename := filepath.Base(escapedRealFilepath)
  151. /*
  152. //12 Jul 2022 Update: Deprecated the browser detection logic
  153. userAgent := r.Header.Get("User-Agent")
  154. if strings.Contains(userAgent, "Safari/")) {
  155. //This is Safari. Use speial header
  156. w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(realFilepath))
  157. } else {
  158. //Fixing the header issue on Golang url encode lib problems
  159. w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+filename)
  160. }
  161. */
  162. w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
  163. w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
  164. if targetFsh.RequireBuffer || !filesystem.FileExists(realFilepath) {
  165. //Stream it directly from remote
  166. w.Header().Set("Content-Length", strconv.Itoa(int(targetFshAbs.GetFileSize(realFilepath))))
  167. remoteStream, err := targetFshAbs.ReadStream(realFilepath)
  168. if err != nil {
  169. common.SendErrorResponse(w, err.Error())
  170. return
  171. }
  172. io.Copy(w, remoteStream)
  173. remoteStream.Close()
  174. } else {
  175. http.ServeFile(w, r, escapedRealFilepath)
  176. }
  177. } else {
  178. if targetFsh.RequireBuffer {
  179. w.Header().Set("Content-Length", strconv.Itoa(int(targetFshAbs.GetFileSize(realFilepath))))
  180. //Check buffer exists
  181. ps, _ := targetFsh.GetUniquePathHash(vpath, userinfo.Username)
  182. buffpool := filepath.Join(*tmp_directory, "fsbuffpool")
  183. buffFile := filepath.Join(buffpool, ps)
  184. if fs.FileExists(buffFile) {
  185. //Stream the buff file if hash matches
  186. remoteFileHash, err := getHashFromRemoteFile(targetFsh.FileSystemAbstraction, realFilepath)
  187. if err == nil {
  188. localFileHash, err := os.ReadFile(buffFile + ".hash")
  189. if err == nil {
  190. if string(localFileHash) == remoteFileHash {
  191. //Hash matches. Serve local buffered file
  192. http.ServeFile(w, r, buffFile)
  193. return
  194. }
  195. }
  196. }
  197. }
  198. remoteStream, err := targetFshAbs.ReadStream(realFilepath)
  199. if err != nil {
  200. common.SendErrorResponse(w, err.Error())
  201. return
  202. }
  203. defer remoteStream.Close()
  204. io.Copy(w, remoteStream)
  205. if *enable_buffering {
  206. os.MkdirAll(buffpool, 0775)
  207. go func() {
  208. BufferRemoteFileToTmp(buffFile, targetFsh, realFilepath)
  209. }()
  210. }
  211. } else if !filesystem.FileExists(realFilepath) {
  212. //Streaming from remote file system that support fseek
  213. f, err := targetFsh.FileSystemAbstraction.Open(realFilepath)
  214. if err != nil {
  215. w.WriteHeader(http.StatusInternalServerError)
  216. w.Write([]byte("500 - Internal Server Error"))
  217. return
  218. }
  219. fstat, _ := f.Stat()
  220. defer f.Close()
  221. http.ServeContent(w, r, filepath.Base(realFilepath), fstat.ModTime(), f)
  222. } else {
  223. http.ServeFile(w, r, realFilepath)
  224. }
  225. }
  226. }
  227. func BufferRemoteFileToTmp(buffFile string, fsh *filesystem.FileSystemHandler, rpath string) error {
  228. if fs.FileExists(buffFile + ".download") {
  229. return errors.New("another buffer process running")
  230. }
  231. //Generate a stat file for the buffer
  232. hash, err := getHashFromRemoteFile(fsh.FileSystemAbstraction, rpath)
  233. if err != nil {
  234. //Do not buffer
  235. return err
  236. }
  237. os.WriteFile(buffFile+".hash", []byte(hash), 0775)
  238. //Buffer the file from remote to local
  239. f, err := fsh.FileSystemAbstraction.ReadStream(rpath)
  240. if err != nil {
  241. os.Remove(buffFile + ".hash")
  242. return err
  243. }
  244. defer f.Close()
  245. dest, err := os.OpenFile(buffFile+".download", os.O_CREATE|os.O_WRONLY, 0775)
  246. if err != nil {
  247. os.Remove(buffFile + ".hash")
  248. return err
  249. }
  250. defer dest.Close()
  251. io.Copy(dest, f)
  252. f.Close()
  253. dest.Close()
  254. os.Rename(buffFile+".download", buffFile)
  255. //Clean the oldest buffpool item if size too large
  256. dirsize, _ := fs.GetDirctorySize(filepath.Dir(buffFile), false)
  257. oldestModtime := time.Now().Unix()
  258. oldestFile := ""
  259. for int(dirsize) > *bufferPoolSize<<20 {
  260. //fmt.Println("CLEARNING BUFF", dirsize)
  261. files, _ := filepath.Glob(filepath.ToSlash(filepath.Dir(buffFile)) + "/*")
  262. for _, file := range files {
  263. if filepath.Ext(file) == ".hash" {
  264. continue
  265. }
  266. thisModTime, _ := fs.GetModTime(file)
  267. if thisModTime < oldestModtime {
  268. oldestModtime = thisModTime
  269. oldestFile = file
  270. }
  271. }
  272. os.Remove(oldestFile)
  273. os.Remove(oldestFile + ".hash")
  274. dirsize, _ = fs.GetDirctorySize(filepath.Dir(buffFile), false)
  275. oldestModtime = time.Now().Unix()
  276. }
  277. return nil
  278. }
  279. func getHashFromRemoteFile(fshAbs filesystem.FileSystemAbstraction, rpath string) (string, error) {
  280. filestat, err := fshAbs.Stat(rpath)
  281. if err != nil {
  282. //Always pull from remote
  283. return "", err
  284. }
  285. if filestat.Size() >= int64(*bufferPoolSize<<20) {
  286. return "", errors.New("Unable to buffer: file larger than buffpool size")
  287. }
  288. if filestat.Size() >= int64(*bufferFileMaxSize<<20) {
  289. return "", errors.New("File larger than max buffer file size")
  290. }
  291. statHash := strconv.Itoa(int(filestat.ModTime().Unix() + filestat.Size()))
  292. hash := md5.Sum([]byte(statHash))
  293. return hex.EncodeToString(hash[:]), nil
  294. }