metadata.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. package metadata
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "sync"
  12. "time"
  13. "github.com/gorilla/websocket"
  14. "imuslab.com/arozos/mod/filesystem"
  15. "imuslab.com/arozos/mod/filesystem/fssort"
  16. hidden "imuslab.com/arozos/mod/filesystem/hidden"
  17. "imuslab.com/arozos/mod/info/logger"
  18. "imuslab.com/arozos/mod/utils"
  19. )
  20. /*
  21. This package is used to extract meta data from files like mp3 and mp4
  22. Also support image caching
  23. */
  24. // RAW image format extensions supported by the system
  25. var RawImageFormats = []string{".arw", ".cr2", ".dng", ".nef", ".raf", ".orf"}
  26. // IsRawImageFile checks if the given file path is a RAW image format
  27. func IsRawImageFile(filePath string) bool {
  28. ext := strings.ToLower(filepath.Ext(filePath))
  29. return utils.StringInArray(RawImageFormats, ext)
  30. }
  31. type RenderHandler struct {
  32. renderingFiles sync.Map
  33. renderingFolder sync.Map
  34. }
  35. // Create a new RenderHandler
  36. func NewRenderHandler() *RenderHandler {
  37. return &RenderHandler{
  38. renderingFiles: sync.Map{},
  39. renderingFolder: sync.Map{},
  40. }
  41. }
  42. // Build cache for all files (non recursive) for the given filepath
  43. func (rh *RenderHandler) BuildCacheForFolder(fsh *filesystem.FileSystemHandler, vpath string, username string) error {
  44. fshAbs := fsh.FileSystemAbstraction
  45. rpath, _ := fshAbs.VirtualPathToRealPath(vpath, username)
  46. //Get a list of all files inside this path
  47. fis, err := fshAbs.ReadDir(filepath.ToSlash(filepath.Clean(rpath)))
  48. if err != nil {
  49. return err
  50. }
  51. for _, fi := range fis {
  52. //Load Cache in generate mode
  53. rh.LoadCache(fsh, filepath.Join(rpath, fi.Name()), true)
  54. }
  55. //Check if the cache folder has file. If not, remove it
  56. cachedFiles, _ := fshAbs.ReadDir(filepath.ToSlash(filepath.Join(filepath.Clean(rpath), "/.metadata/.cache/")))
  57. if len(cachedFiles) == 0 {
  58. fshAbs.RemoveAll(filepath.ToSlash(filepath.Join(filepath.Clean(rpath), "/.metadata/.cache/")) + "/")
  59. }
  60. return nil
  61. }
  62. func (rh *RenderHandler) LoadCacheAsBytes(fsh *filesystem.FileSystemHandler, vpath string, username string, generateOnly bool) ([]byte, error) {
  63. fshAbs := fsh.FileSystemAbstraction
  64. rpath, _ := fshAbs.VirtualPathToRealPath(vpath, username)
  65. b64, err := rh.LoadCache(fsh, rpath, generateOnly)
  66. if err != nil {
  67. return []byte{}, err
  68. }
  69. resultingBytes, _ := base64.StdEncoding.DecodeString(b64)
  70. return resultingBytes, nil
  71. }
  72. // Try to load a cache from file. If not exists, generate it now
  73. func (rh *RenderHandler) LoadCache(fsh *filesystem.FileSystemHandler, rpath string, generateOnly bool) (string, error) {
  74. //Create a cache folder
  75. fshAbs := fsh.FileSystemAbstraction
  76. cacheFolder := filepath.ToSlash(filepath.Join(filepath.Clean(filepath.Dir(rpath)), "/.metadata/.cache/") + "/")
  77. fshAbs.MkdirAll(cacheFolder, 0755)
  78. hidden.HideFile(filepath.Dir(filepath.Clean(cacheFolder)))
  79. hidden.HideFile(cacheFolder)
  80. needsGen, cacheData, err := rh.checkCacheNeeded(fsh, rpath, generateOnly, cacheFolder)
  81. if err != nil {
  82. return "", err
  83. }
  84. if !needsGen {
  85. return cacheData, nil
  86. }
  87. //Generate
  88. return rh.generateCache(fsh, cacheFolder, rpath, generateOnly)
  89. }
  90. // checkCacheNeeded checks if cache generation is needed
  91. func (rh *RenderHandler) checkCacheNeeded(fsh *filesystem.FileSystemHandler, rpath string, generateOnly bool, cacheFolder string) (needsGeneration bool, cacheData string, err error) {
  92. if CacheExists(fsh, rpath) {
  93. if generateOnly {
  94. return false, "", nil
  95. }
  96. ext := ".jpg"
  97. if !fsh.FileSystemAbstraction.FileExists(cacheFolder + filepath.Base(rpath) + ".jpg") {
  98. ext = ".png"
  99. }
  100. folderModeTime, _ := fsh.FileSystemAbstraction.GetModTime(rpath)
  101. cacheImageModeTime, _ := fsh.FileSystemAbstraction.GetModTime(cacheFolder + filepath.Base(rpath) + ext)
  102. if folderModeTime > cacheImageModeTime {
  103. fsh.FileSystemAbstraction.Remove(cacheFolder + filepath.Base(rpath) + ext)
  104. return true, "", nil
  105. } else {
  106. counter := 0
  107. for rh.fileIsBusy(rpath) && counter < 15 {
  108. counter += 1
  109. time.Sleep(1 * time.Second)
  110. }
  111. if rh.fileIsBusy(rpath) {
  112. return false, "", errors.New("process racing for cache file")
  113. }
  114. ctx, err := getImageAsBase64(fsh, cacheFolder+filepath.Base(rpath)+ext)
  115. return false, ctx, err
  116. }
  117. } else if fsh.ReadOnly {
  118. return false, "", errors.New("cannot generate thumbnail on readonly file system")
  119. } else {
  120. if rh.fileIsBusy(rpath) {
  121. return false, "", errors.New("process racing for cache file")
  122. }
  123. }
  124. return true, "", nil
  125. }
  126. func (rh *RenderHandler) generateCache(fsh *filesystem.FileSystemHandler, cacheFolder string, rpath string, generateOnly bool) (string, error) {
  127. //Cache image not exists. Set this file to busy
  128. rh.renderingFiles.Store(rpath, "busy")
  129. //That object not exists. Generate cache image
  130. //Audio formats that might contains id4 thumbnail
  131. id4Formats := []string{".mp3", ".ogg", ".flac"}
  132. if utils.StringInArray(id4Formats, strings.ToLower(filepath.Ext(rpath))) {
  133. img, err := generateThumbnailForAudio(fsh, cacheFolder, rpath, generateOnly)
  134. rh.renderingFiles.Delete(rpath)
  135. return img, err
  136. }
  137. //Generate resized image for images
  138. imageFormats := []string{".png", ".jpeg", ".jpg", ".webp"}
  139. if utils.StringInArray(imageFormats, strings.ToLower(filepath.Ext(rpath))) {
  140. img, err := generateThumbnailForImage(fsh, cacheFolder, rpath, generateOnly)
  141. rh.renderingFiles.Delete(rpath)
  142. return img, err
  143. }
  144. //RAW image formats (Sony, Canon, Nikon, etc.)
  145. if IsRawImageFile(rpath) {
  146. img, err := generateThumbnailForRAW(fsh, cacheFolder, rpath, generateOnly)
  147. rh.renderingFiles.Delete(rpath)
  148. return img, err
  149. }
  150. //Video formats, extract from the 5 sec mark
  151. vidFormats := []string{".mkv", ".mp4", ".webm", ".ogv", ".avi", ".rmvb"}
  152. if utils.StringInArray(vidFormats, strings.ToLower(filepath.Ext(rpath))) {
  153. img, err := generateThumbnailForVideo(fsh, cacheFolder, rpath, generateOnly)
  154. rh.renderingFiles.Delete(rpath)
  155. return img, err
  156. }
  157. //3D Model Formats
  158. modelFormats := []string{".stl", ".obj"}
  159. if utils.StringInArray(modelFormats, strings.ToLower(filepath.Ext(rpath))) {
  160. img, err := generateThumbnailForModel(fsh, cacheFolder, rpath, generateOnly)
  161. rh.renderingFiles.Delete(rpath)
  162. return img, err
  163. }
  164. //Sliced G-code, which carries a slicer generated preview in its header
  165. gcodeFormats := []string{".gcode", ".gco"}
  166. if utils.StringInArray(gcodeFormats, strings.ToLower(filepath.Ext(rpath))) {
  167. img, err := generateThumbnailForGcode(fsh, cacheFolder, rpath, generateOnly)
  168. rh.renderingFiles.Delete(rpath)
  169. return img, err
  170. }
  171. //Photoshop file
  172. if strings.ToLower(filepath.Ext(rpath)) == ".psd" {
  173. img, err := generateThumbnailForPSD(fsh, cacheFolder, rpath, generateOnly)
  174. rh.renderingFiles.Delete(rpath)
  175. return img, err
  176. }
  177. //SVG file
  178. if strings.ToLower(filepath.Ext(rpath)) == ".svg" {
  179. img, err := generateThumbnailForSVG(fsh, cacheFolder, rpath, generateOnly)
  180. rh.renderingFiles.Delete(rpath)
  181. return img, err
  182. }
  183. //Folder preview renderer
  184. if fsh.FileSystemAbstraction.IsDir(rpath) && len(filepath.Base(rpath)) > 0 && filepath.Base(rpath)[:1] != "." {
  185. img, err := generateThumbnailForFolder(fsh, cacheFolder, rpath, generateOnly)
  186. rh.renderingFiles.Delete(rpath)
  187. return img, err
  188. }
  189. //Other filters
  190. rh.renderingFiles.Delete(rpath)
  191. return "", errors.New("no supported format")
  192. }
  193. func (rh *RenderHandler) fileIsBusy(path string) bool {
  194. if rh == nil {
  195. logger.PrintAndLog("Metadata", "RenderHandler is null!", nil)
  196. return true
  197. }
  198. _, ok := rh.renderingFiles.Load(path)
  199. if !ok {
  200. //File path is not being process by another process
  201. return false
  202. } else {
  203. return true
  204. }
  205. }
  206. func getImageAsBase64(fsh *filesystem.FileSystemHandler, rpath string) (string, error) {
  207. fshAbs := fsh.FileSystemAbstraction
  208. content, err := fshAbs.ReadFile(rpath)
  209. if err != nil {
  210. return "", err
  211. }
  212. encoded := base64.StdEncoding.EncodeToString(content)
  213. return string(encoded), nil
  214. }
  215. // Load a list of folder cache from websocket, pass in "" (empty string) for default sorting method
  216. func (rh *RenderHandler) HandleLoadCache(w http.ResponseWriter, r *http.Request, fsh *filesystem.FileSystemHandler, rpath string, sortmode string) {
  217. //Get a list of files pending to be cached and sent
  218. targetPath := filepath.ToSlash(filepath.Clean(rpath))
  219. //Check if this path already exists another websocket ongoing connection.
  220. //If yes, disconnect the oldone
  221. oldc, ok := rh.renderingFolder.Load(targetPath)
  222. if ok {
  223. //Close and remove the old connection
  224. oldc.(*websocket.Conn).Close()
  225. }
  226. fis, err := fsh.FileSystemAbstraction.ReadDir(targetPath)
  227. if err != nil {
  228. w.WriteHeader(http.StatusInternalServerError)
  229. w.Write([]byte("500 - Internal Server Error"))
  230. return
  231. }
  232. //Upgrade the connection to websocket
  233. var upgrader = websocket.Upgrader{}
  234. upgrader.CheckOrigin = func(r *http.Request) bool { return true }
  235. c, err := upgrader.Upgrade(w, r, nil)
  236. if err != nil {
  237. logger.PrintAndLog("Metadata", fmt.Sprint("upgrade:", err), nil)
  238. w.WriteHeader(http.StatusInternalServerError)
  239. w.Write([]byte("500 - Internal Server Error"))
  240. return
  241. }
  242. //Set this realpath as websocket connected
  243. rh.renderingFolder.Store(targetPath, c)
  244. //For each file, serve a cached image preview
  245. errorExists := false
  246. filesWithoutCache := []string{}
  247. //Updates implementation 02/10/2021: Load thumbnail of files first before folder and apply user preference sort mode
  248. if sortmode == "" {
  249. sortmode = "default"
  250. }
  251. sortedFis := fssort.SortDirEntryList(fis, sortmode)
  252. pendingFiles := []string{}
  253. pendingFolders := []string{}
  254. for _, fileInfo := range sortedFis {
  255. if !fileInfo.IsDir() {
  256. pendingFiles = append(pendingFiles, filepath.Join(targetPath, fileInfo.Name()))
  257. } else {
  258. pendingFolders = append(pendingFolders, filepath.Join(targetPath, fileInfo.Name()))
  259. }
  260. }
  261. pendingFiles = append(pendingFiles, pendingFolders...)
  262. files := pendingFiles
  263. //Updated implementation 24/12/2020: Load image with cache first before rendering those without
  264. for _, file := range files {
  265. if !CacheExists(fsh, file) {
  266. //Cache not exists. Render this later
  267. filesWithoutCache = append(filesWithoutCache, file)
  268. } else {
  269. //Cache exists. Send it out first
  270. cachedImage, err := rh.LoadCache(fsh, file, false)
  271. if err != nil {
  272. } else {
  273. jsonString, _ := json.Marshal([]string{filepath.Base(file), cachedImage})
  274. err := c.WriteMessage(1, jsonString)
  275. if err != nil {
  276. //Connection closed
  277. errorExists = true
  278. break
  279. }
  280. }
  281. }
  282. }
  283. retryList := []string{}
  284. //Render the remaining cache files
  285. for _, file := range filesWithoutCache {
  286. //Load the image cache
  287. cachedImage, err := rh.LoadCache(fsh, file, false)
  288. if err != nil {
  289. //Unable to load this file's cache. Push it to retry list
  290. retryList = append(retryList, file)
  291. } else {
  292. jsonString, _ := json.Marshal([]string{filepath.Base(file), cachedImage})
  293. err := c.WriteMessage(1, jsonString)
  294. if err != nil {
  295. //Connection closed
  296. errorExists = true
  297. break
  298. }
  299. }
  300. }
  301. //Process the retry list after some wait time
  302. if len(retryList) > 0 {
  303. time.Sleep(1000 * time.Millisecond)
  304. for _, file := range retryList {
  305. //Load the image cache
  306. cachedImage, err := rh.LoadCache(fsh, file, false)
  307. if err != nil {
  308. } else {
  309. jsonString, _ := json.Marshal([]string{filepath.Base(file), cachedImage})
  310. err := c.WriteMessage(1, jsonString)
  311. if err != nil {
  312. //Connection closed
  313. errorExists = true
  314. break
  315. }
  316. }
  317. }
  318. }
  319. //Clear record from syncmap
  320. if !errorExists {
  321. //This ended normally. Delete the targetPath
  322. rh.renderingFolder.Delete(targetPath)
  323. }
  324. c.Close()
  325. }
  326. // Check if the cache for a file exists
  327. func CacheExists(fsh *filesystem.FileSystemHandler, file string) bool {
  328. cacheFolder := filepath.ToSlash(filepath.Join(filepath.Clean(filepath.Dir(file)), "/.metadata/.cache/") + "/")
  329. return fsh.FileSystemAbstraction.FileExists(cacheFolder+filepath.Base(file)+".jpg") || fsh.FileSystemAbstraction.FileExists(cacheFolder+filepath.Base(file)+".png")
  330. }
  331. // Get cache path for this file, given realpath
  332. func GetCacheFilePath(fsh *filesystem.FileSystemHandler, file string) (string, error) {
  333. if CacheExists(fsh, file) {
  334. fshAbs := fsh.FileSystemAbstraction
  335. cacheFolder := filepath.ToSlash(filepath.Join(filepath.Clean(filepath.Dir(file)), "/.metadata/.cache/") + "/")
  336. if fshAbs.FileExists(cacheFolder + filepath.Base(file) + ".jpg") {
  337. return cacheFolder + filepath.Base(file) + ".jpg", nil
  338. } else if fshAbs.FileExists(cacheFolder + filepath.Base(file) + ".png") {
  339. return cacheFolder + filepath.Base(file) + ".png", nil
  340. } else {
  341. return "", errors.New("Unable to resolve thumbnail cache location")
  342. }
  343. } else {
  344. return "", errors.New("No thumbnail cached for this file")
  345. }
  346. }
  347. // Remove cache if exists, given realpath
  348. func RemoveCache(fsh *filesystem.FileSystemHandler, file string) error {
  349. if CacheExists(fsh, file) {
  350. cachePath, err := GetCacheFilePath(fsh, file)
  351. if err != nil {
  352. return err
  353. }
  354. //Remove the thumbnail cache
  355. os.Remove(cachePath)
  356. return nil
  357. } else {
  358. return errors.New("Thumbnail cache not exists for this file")
  359. }
  360. }