func moveFiles(local_path string, id int64, newImage *Image) (werr *web.Error) { // Do some local image processing first: var firstImage image.Image var err error firstImage, newImage.Kind, err = decodeFirstImage(local_path) defer func() { firstImage = nil }() if werr = web.AsError(err, http.StatusInternalServerError); werr != nil { return } _, ext, thumbExt := imageKindTo(newImage.Kind) // Move the file into the store folder: if werr = moveToStoreFolder(local_path, id, ext); werr != nil { return } // Generate a thumbnail: img_name := strconv.FormatInt(id, 10) os.MkdirAll(thumb_folder(), 0755) thumb_path := path.Join(thumb_folder(), img_name+thumbExt) if werr = web.AsError(generateThumbnail(firstImage, newImage.Kind, thumb_path), http.StatusInternalServerError); werr != nil { return } return nil }
func storeImage(req *imageStoreRequest) (id int64, werr *web.Error) { if req.Title == "" { return 0, web.AsError(fmt.Errorf("Missing title!"), http.StatusBadRequest) } // Open the database: werr = useAPI(func(api *API) (werr *web.Error) { var err error newImage := &Image{ Kind: req.Kind, Title: req.Title, SourceURL: &req.SourceURL, CollectionName: req.CollectionName, Submitter: req.Submitter, IsClean: req.IsClean, Keywords: strings.ToLower(req.Keywords), } // Generate keywords from title: if newImage.Keywords == "" { newImage.Keywords = titleToKeywords(newImage.Title) } if newImage.Kind == "" { newImage.Kind = "gif" } // Create the DB record: id, err = api.NewImage(newImage) if werr = web.AsError(err, http.StatusInternalServerError); werr != nil { return } // Run post-creation function: if req.PostCreation != nil { if werr = req.PostCreation(id, newImage); werr != nil { return } } // Update image record with new Kind or other information discovered after download: err = api.Update(newImage) if werr = web.AsError(err, http.StatusInternalServerError); werr != nil { //log.Println(err) return } return nil }) if werr != nil { return 0, werr } return id, nil }
func listCollection(rsp http.ResponseWriter, req *http.Request, keywords []string, collectionName string, list []Image, nsfw bool) { cached, werr := doCaching(req, rsp, struct { CollectionName string List []Image ShowUnclean bool }{ CollectionName: collectionName, List: list, ShowUnclean: nsfw, }) if werr != nil { return } if cached { return } // Project into a view model: model := struct { List []ImageViewModel ShowUnclean bool Keywords string }{ List: projectModelList(list), ShowUnclean: nsfw, Keywords: strings.Join(keywords, " "), } rsp.Header().Set("Content-Type", "text/html; charset=utf-8") rsp.WriteHeader(200) if web.AsError(uiTmpl.ExecuteTemplate(rsp, "list", model), http.StatusInternalServerError).AsHTML().Respond(rsp) { return } return }
func useAPI(use func(api *API) *web.Error) *web.Error { api, err := NewAPI() if err != nil { return web.AsError(err, http.StatusInternalServerError) } defer api.Close() return use(api) }
func moveToStoreFolder(local_path string, id int64, ext string) (werr *web.Error) { // Move and rename the file: img_name := strconv.FormatInt(id, 10) os.MkdirAll(store_folder(), 0755) store_path := path.Join(store_folder(), img_name+ext) if werr = web.AsError(os.Rename(local_path, store_path), http.StatusInternalServerError); werr != nil { return } return nil }
func apiSearch(keywords []string, collectionName string, includeBase bool, orderBy ImagesOrderBy) (list []Image, werr *web.Error) { werr = useAPI(func(api *API) *web.Error { var err error list, err = api.Search(keywords, collectionName, includeBase, orderBy) return web.AsError(err, http.StatusInternalServerError) }) return }
func getImage(id int64) (img *Image, werr *web.Error) { werr = useAPI(func(api *API) *web.Error { var err error img, err = api.GetImage(id) return web.AsError(err, http.StatusInternalServerError) }) return }
func downloadFile(url string) (string, *web.Error) { // Do a HTTP GET to fetch the image: img_rsp, err := http.Get(url) if err != nil { return "", web.AsError(err, http.StatusInternalServerError) } defer img_rsp.Body.Close() // Create a local temporary file to download to: os.MkdirAll(tmp_folder(), 0755) local_file, err := TempFile(tmp_folder(), "dl-", "") if err != nil { return "", web.AsError(err, http.StatusInternalServerError) } defer local_file.Close() // Download file: _, err = io.Copy(local_file, img_rsp.Body) if err != nil { return "", web.AsError(err, http.StatusInternalServerError) } return local_file.Name(), nil }
func doCaching(req *http.Request, rsp http.ResponseWriter, data interface{}) (bool, *web.Error) { // Calculate ETag of data as hex(SHA256(gob(data))): sha := sha256.New() err := gob.NewEncoder(sha).Encode(data) if err != nil { return false, web.AsError(err, http.StatusInternalServerError) } etag := "\"" + hex.EncodeToString(sha.Sum(nil)) + "\"" if check_etag := req.Header.Get("If-None-Match"); check_etag == etag { // 304 Not Modified: rsp.WriteHeader(http.StatusNotModified) return true, nil } rsp.Header().Set("ETag", etag) return false, nil }
// Serves an index.html file for a directory or sends the requested file. func processRequest(rsp http.ResponseWriter, req *http.Request) *web.Error { // proxy sends us absolute path URLs u, err := url.Parse(req.RequestURI) if err != nil { return web.AsError(err, 500) } if (jplayerPath != "") && strings.HasPrefix(u.Path, jplayerUrl) { // URL is under the jPlayer path: localPath := path.Join(jplayerPath, removeIfStartsWith(u.Path, jplayerUrl)) http.ServeFile(rsp, req, localPath) return nil } else if strings.HasPrefix(u.Path, proxyRoot) { // URL is under the proxy path: return processProxiedRequest(rsp, req, u) } return nil }
// handles requests to upload images and rehost with shortened URLs func requestHandler(rsp http.ResponseWriter, req *http.Request) *web.Error { // Set RemoteAddr for forwarded requests: { ip := req.Header.Get("X-Real-IP") if ip == "" { ip = req.Header.Get("X-Forwarded-For") } if ip != "" { req.RemoteAddr = ip } } //log.Printf("%s %s %s %s\nHeaders: %v\n\n", req.RemoteAddr, req.Method, req.Host, req.URL, req.Header) if req.Method == "POST" { // POST: if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/col/add"); ok { // Add a new image via URL to download from: imgurl_s := req.FormValue("url") if imgurl_s == "" { return web.AsError(fmt.Errorf("Missing required 'url' form value!"), http.StatusBadRequest).AsHTML() } log.Printf("nsfw='%s'\n", req.FormValue("nsfw")) nsfw := req.FormValue("nsfw") == "1" store := &imageStoreRequest{ CollectionName: collectionName, Submitter: req.RemoteAddr, Title: req.FormValue("title"), SourceURL: imgurl_s, Keywords: strings.ToLower(req.FormValue("keywords")), IsClean: !nsfw, } // Require the 'title' form value: if store.Title == "" { return web.AsError(fmt.Errorf("Missing title!"), http.StatusBadRequest).AsHTML() } // Download the image from the URL: if werr := downloadImageFor(store); werr != nil { return werr.AsHTML() } // Store it in the database and generate thumbnail: id, werr := storeImage(store) if werr != nil { return werr.AsHTML() } // Redirect to a black-background view of the image: redir_url := path.Join("/b/", b62.Encode(id+10000)) http.Redirect(rsp, req, redir_url, http.StatusFound) return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/col/upload"); ok { // Upload a new image: _ = "breakpoint" store := &imageStoreRequest{ CollectionName: collectionName, Submitter: req.RemoteAddr, } if !web.IsMultipart(req) { return web.AsError(fmt.Errorf("Upload request must be multipart form data"), http.StatusBadRequest).AsHTML() } // Accept file upload: reader, err := req.MultipartReader() if werr := web.AsError(err, http.StatusBadRequest); werr != nil { return werr.AsHTML() } // Keep reading the multipart form data and handle file uploads: for { part, err := reader.NextPart() if err == io.EOF { break } // Parse normal form values: if part.FormName() == "title" { // TODO: parse content-length if it exists? //part.Header.Get("Content-Length") t, err := ioutil.ReadAll(part) if werr := web.AsError(err, http.StatusInternalServerError); werr != nil { return werr.AsHTML() } store.Title = string(t) continue } else if part.FormName() == "keywords" { t, err := ioutil.ReadAll(part) if werr := web.AsError(err, http.StatusInternalServerError); werr != nil { return werr.AsHTML() } store.Keywords = strings.ToLower(string(t)) continue } else if part.FormName() == "nsfw" { t, err := ioutil.ReadAll(part) if werr := web.AsError(err, http.StatusInternalServerError); werr != nil { return werr.AsHTML() } nsfw := (string(t) == "1") store.IsClean = !nsfw continue } if part.FileName() == "" { continue } // Copy upload data to a local file: store.SourceURL = "file://" + part.FileName() werr := func() *web.Error { os.MkdirAll(tmp_folder(), 0755) f, err := TempFile(tmp_folder(), "up-", path.Ext(part.FileName())) if err != nil { return web.AsError(err, http.StatusInternalServerError) } defer f.Close() local_path := f.Name() store.PostCreation = func(id int64, newImage *Image) (werr *web.Error) { return moveFiles(local_path, id, newImage) } if _, err := io.Copy(f, part); err != nil { return web.AsError(err, http.StatusInternalServerError) } return nil }() if werr.AsHTML().Respond(rsp) { return werr } } // Store it in the database and generate thumbnail: id, werr := storeImage(store) if werr.AsHTML().Respond(rsp) { return werr } // Redirect to a black-background view of the image: redir_url := path.Join("/b/", b62.Encode(id+10000)) http.Redirect(rsp, req, redir_url, http.StatusFound) return nil } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/admin/download"); ok { id := b62.Decode(id_s) - 10000 var img *Image if werr := useAPI(func(api *API) *web.Error { var err error img, err = api.GetImage(id) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsHTML() } if img == nil { return web.AsError(fmt.Errorf("Could not find image by ID"), http.StatusNotFound).AsHTML() } // Download the image: storeRequest := &imageStoreRequest{ Kind: img.Kind, Title: img.Title, SourceURL: *img.SourceURL, Submitter: img.Submitter, IsClean: img.IsClean, Keywords: img.Keywords, CollectionName: img.CollectionName, } if werr := downloadImageFor(storeRequest); werr != nil { return werr.AsHTML() } if werr := storeRequest.PostCreation(img.ID, img); werr != nil { return werr.AsHTML() } // Update the image record: img.Kind = storeRequest.Kind img.SourceURL = &storeRequest.SourceURL // Process the update request: if werr := useAPI(func(api *API) *web.Error { return web.AsError(api.Update(img), http.StatusInternalServerError) }); werr != nil { return werr.AsHTML() } // Redirect back to edit page: http.Redirect(rsp, req, "/admin/edit/"+id_s, http.StatusFound) return nil } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/admin/update"); ok { id := b62.Decode(id_s) - 10000 if req.FormValue("delete") != "" { if werr := useAPI(func(api *API) *web.Error { var err error err = api.Delete(id) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsHTML() } // Redirect back to edit page: http.Redirect(rsp, req, "/admin/edit/"+id_s, http.StatusFound) return nil } var img *Image if werr := useAPI(func(api *API) *web.Error { var err error img, err = api.GetImage(id) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsHTML() } if img == nil { return web.AsError(fmt.Errorf("Could not find image by ID"), http.StatusNotFound).AsHTML() } img.Title = req.FormValue("title") img.Keywords = strings.ToLower(req.FormValue("keywords")) img.CollectionName = req.FormValue("collection") img.Submitter = req.FormValue("submitter") img.IsClean = (req.FormValue("nsfw") == "") img.Kind = req.FormValue("kind") // Generate keywords from title: if img.Keywords == "" { img.Keywords = titleToKeywords(img.Title) } // Process the update request: if werr := useAPI(func(api *API) *web.Error { return web.AsError(api.Update(img), http.StatusInternalServerError) }); werr != nil { return werr.AsHTML() } // Redirect back to edit page: http.Redirect(rsp, req, "/admin/edit/"+id_s, http.StatusFound) return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/add"); ok { // Add a new image via URL to download from via JSON API: store := &imageStoreRequest{ CollectionName: collectionName, } jd := json.NewDecoder(req.Body) err := jd.Decode(store) if werr := web.AsError(err, http.StatusBadRequest); werr != nil { return werr.AsJSON() } // Download Image locally: if werr := downloadImageFor(store); werr != nil { return werr.AsJSON() } // Process the store request: id, werr := storeImage(store) if werr != nil { return werr.AsJSON() } web.JsonSuccess(rsp, &struct { ID int64 `json:"id"` Base62ID string `json:"base62id"` }{ ID: id, Base62ID: b62.Encode(id + 10000), }) return nil } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/update"); ok { // TODO: Lock down based on basic_auth.username == collectionName. id := b62.Decode(id_s) - 10000 // Get existing image for update: img, werr := getImage(id) if werr != nil { return werr.AsJSON() } // Decode JSON straight onto existing Image record: jd := json.NewDecoder(req.Body) err := jd.Decode(img) if werr := web.AsError(err, http.StatusBadRequest); werr != nil { return werr.AsJSON() } // Generate keywords from title: if img.Keywords == "" { img.Keywords = titleToKeywords(img.Title) } else { img.Keywords = strings.ToLower(img.Keywords) } // Process the update request: if werr := useAPI(func(api *API) *web.Error { return web.AsError(api.Update(img), http.StatusInternalServerError) }); werr != nil { return werr.AsJSON() } web.JsonSuccess(rsp, &struct { Success bool `json:"success"` }{ Success: true, }) return nil } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/delete"); ok { // TODO: Lock down based on basic_auth.username == collectionName. id := b62.Decode(id_s) - 10000 if werr := useAPI(func(api *API) *web.Error { return web.AsError(api.Delete(id), http.StatusInternalServerError) }); werr != nil { return werr.AsJSON() } web.JsonSuccess(rsp, &struct { Success bool `json:"success"` }{ Success: true, }) return nil } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/crop"); ok { id := b62.Decode(id_s) - 10000 cr := &struct { Left int `json:"left"` Top int `json:"top"` Right int `json:"right"` Bottom int `json:"bottom"` }{} jd := json.NewDecoder(req.Body) err := jd.Decode(cr) if werr := web.AsError(err, http.StatusBadRequest); werr != nil { return werr.AsJSON() } var img *Image if werr := useAPI(func(api *API) *web.Error { var err error img, err = api.GetImage(id) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsJSON() } if img == nil { return web.AsError(fmt.Errorf("Could not find image by ID"), http.StatusNotFound).AsJSON() } // Crop the image: // _, ext, thumbExt := imageKindTo(img.Kind) _, ext, _ := imageKindTo(img.Kind) local_path := path.Join(store_folder(), strconv.FormatInt(img.ID, 10)+ext) tmp_output, err := cropImage(local_path, cr.Left, cr.Top, cr.Right, cr.Bottom) if werr := web.AsError(err, http.StatusInternalServerError); werr != nil { return werr.AsJSON() } // Clone the image record to a new record: if werr := useAPI(func(api *API) *web.Error { var err error img.ID = 0 img.ID, err = api.NewImage(img) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsJSON() } // Move the temp file to the final storage path: img_name := strconv.FormatInt(img.ID, 10) os.MkdirAll(store_folder(), 0755) store_path := path.Join(store_folder(), img_name+ext) if werr := web.AsError(os.Rename(tmp_output, store_path), http.StatusInternalServerError); werr != nil { return werr.AsJSON() } //// Generate a thumbnail: //os.MkdirAll(thumb_folder(), 0755) //thumb_path := path.Join(thumb_folder(), img_name+thumbExt) //if web.AsError(generateThumbnail(firstImage, newImage.Kind, thumb_path), http.StatusInternalServerError).AsJSON().Respond(rsp) { // return //} width, height := cr.Right-cr.Left, cr.Bottom-cr.Top web.JsonSuccess(rsp, &struct { ID int64 `json:"id"` Base62ID string `json:"base62id"` Title string `json:"title"` CollectionName string `json:"collectionName,omitempty"` Submitter string `json:"submitter,omitempty"` Kind string `json:"kind"` Width *int `json:"width,omitempty"` Height *int `json:"height,omitempty"` }{ ID: img.ID, Base62ID: b62.Encode(img.ID + 10000), Kind: img.Kind, Title: img.Title, CollectionName: img.CollectionName, Submitter: img.Submitter, Width: &width, Height: &height, }) return nil } rsp.WriteHeader(http.StatusBadRequest) return nil } // GET: req_query := req.URL.Query() nsfw := false nsfw_s := req_query.Get("nsfw") if nsfw_s != "" { nsfw = true } var orderBy ImagesOrderBy if _, ok := req_query["title"]; ok { orderBy = ImagesOrderByTitleASC } else if _, ok := req_query["oldest"]; ok { orderBy = ImagesOrderByIDASC } else { orderBy = ImagesOrderByIDDESC } if req.URL.Path == "/favicon.ico" { return web.NewError(nil, http.StatusNoContent, web.Empty) } else if req.URL.Path == "/" { keywords := normalizeKeywords(req_query["q"]) list, werr := apiSearch(keywords, "all", true, orderBy) if werr != nil { return werr.AsHTML() } listCollection(rsp, req, keywords, "", list, nsfw) return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/col/list"); ok { keywords := normalizeKeywords(req_query["q"]) list, werr := apiSearch(keywords, collectionName, true, orderBy) if werr != nil { return werr.AsHTML() } listCollection(rsp, req, keywords, collectionName, list, nsfw) return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/col/only"); ok { keywords := normalizeKeywords(req_query["q"]) list, werr := apiSearch(keywords, collectionName, false, orderBy) if werr != nil { return werr.AsHTML() } listCollection(rsp, req, keywords, collectionName, list, nsfw) return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/col/add"); ok { model := &struct { AddURL string UploadURL string }{} model.AddURL = "/col/add" model.UploadURL = "/col/upload" if collectionName != "" { model.AddURL = "/col/add/" + collectionName model.UploadURL = "/col/upload/" + collectionName } // GET the /col/add form to add a new image: rsp.Header().Set("Content-Type", "text/html; charset=utf-8") rsp.WriteHeader(200) if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "new", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if web.MatchExactRouteIgnoreSlash(req.URL.Path, "/admin") { keywords := normalizeKeywords(req_query["q"]) list, werr := apiSearch(keywords, "all", true, orderBy) if werr != nil { return werr.AsHTML() } // Project into a view model: model := struct { List []ImageViewModel Keywords string }{ List: projectModelList(list), Keywords: strings.Join(keywords, " "), } // GET the /admin/list to link to edit pages: rsp.Header().Set("Content-Type", "text/html; charset=utf-8") rsp.WriteHeader(200) if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "admin", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/admin/list"); ok { keywords := normalizeKeywords(req_query["q"]) list, werr := apiSearch(keywords, collectionName, true, orderBy) if werr != nil { return werr.AsHTML() } // Project into a view model: model := struct { List []ImageViewModel Keywords string }{ List: projectModelList(list), Keywords: strings.Join(keywords, " "), } // GET the /admin/list to link to edit pages: rsp.Header().Set("Content-Type", "text/html; charset=utf-8") rsp.WriteHeader(200) if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "admin", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/admin/edit"); ok { id := b62.Decode(id_s) - 10000 var img *Image if werr := useAPI(func(api *API) *web.Error { var err error img, err = api.GetImage(id) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsHTML() } if img == nil { return web.AsError(fmt.Errorf("Could not find image by ID"), http.StatusNotFound).AsHTML() } // Project into a view model: model := viewTemplateModel{ BGColor: "gray", Query: flattenQuery(req_query), Image: *xlatImageViewModel(img, nil), // Allow editing: IsAdmin: true, } // GET the /admin/list to link to edit pages: rsp.Header().Set("Content-Type", "text/html; charset=utf-8") rsp.WriteHeader(200) if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "view", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if v_id, ok := web.MatchSimpleRoute(req.URL.Path, "/view/yt"); ok { // GET /view/yt/<video_id> to display a youtube player page for <video_id>, e.g. `dQw4w9WgXcQ` model := viewTemplateModel{ BGColor: "black", FillScreen: true, Query: flattenQuery(req_query), Image: *xlatImageViewModel(&Image{ ID: int64(0), Kind: "youtube", Title: v_id, SourceURL: &v_id, CollectionName: "", Submitter: "", RedirectToID: nil, IsHidden: true, IsClean: false, Keywords: "", }, nil), } // Set controls=1 if it's missing: if _, ok := model.Query["controls"]; !ok { model.Query["controls"] = "1" } rsp.Header().Set("Content-Type", "text/html; charset=utf-8") if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "view", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if imgurl, ok := web.MatchSimpleRouteRaw(req.URL.Path, "/view/img/"); ok { // GET /view/img/<imgurl> to display an image viewer page for any URL <imgurl>, e.g. `//` model := viewTemplateModel{ BGColor: "black", Query: flattenQuery(req_query), Image: ImageViewModel{ ID: int64(0), Base62ID: "_", Title: imgurl, Kind: "jpeg", ImageURL: imgurl, ThumbURL: "", Submitter: "", CollectionName: "", SourceURL: &imgurl, RedirectToID: nil, IsClean: false, Keywords: "", }, } rsp.Header().Set("Content-Type", "text/html; charset=utf-8") if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "view", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/list"); ok { // `/api/v1/list/all` returns all images across all collections. list, werr := getList(collectionName, true, orderBy) return apiListResult(req, rsp, list, werr) } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/only"); ok { list, werr := getList(collectionName, false, orderBy) return apiListResult(req, rsp, list, werr) } else if collectionName, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/search"); ok { // Join and resplit keywords by spaces because `req_query["q"]` splits at `q=1&q=2&q=3` level, not spaces. keywords := normalizeKeywords(req_query["q"]) list, werr := apiSearch(keywords, collectionName, true, orderBy) return apiListResult(req, rsp, list, werr) } else if id_s, ok := web.MatchSimpleRoute(req.URL.Path, "/api/v1/info"); ok { id := b62.Decode(id_s) - 10000 var img *Image if werr := useAPI(func(api *API) *web.Error { var err error img, err = api.GetImage(id) return web.AsError(err, http.StatusInternalServerError) }); werr != nil { return werr.AsJSON() } if img == nil { return web.AsError(fmt.Errorf("Could not find image by ID"), http.StatusNotFound).AsJSON() } // Decode the image and grab its properties: _, ext, _ := imageKindTo(img.Kind) local_path := path.Join(store_folder(), strconv.FormatInt(img.ID, 10)+ext) model := &struct { ID int64 `json:"id"` Base62ID string `json:"base62id"` Title string `json:"title"` Keywords string `json:"keywords"` CollectionName string `json:"collectionName,omitempty"` Submitter string `json:"submitter,omitempty"` Kind string `json:"kind"` SourceURL *string `json:"sourceURL,omitempty"` RedirectToID *int64 `json:"redirectToID,omitempty"` Width *int `json:"width,omitempty"` Height *int `json:"height,omitempty"` }{ ID: id, Base62ID: b62.Encode(id + 10000), Kind: img.Kind, Title: img.Title, Keywords: img.Keywords, CollectionName: img.CollectionName, Submitter: img.Submitter, SourceURL: img.SourceURL, RedirectToID: img.RedirectToID, } if model.Kind == "" { model.Kind = "gif" } if model.Kind != "youtube" { var width, height int var err error width, height, model.Kind, err = getImageInfo(local_path) if err != nil { log.Println(err) } else { model.Width = &width model.Height = &height } } web.JsonSuccess(rsp, model) return nil } dir := path.Dir(req.URL.Path) // Look up the image's record by base62 encoded ID: filename := path.Base(req.URL.Path) req_ext := path.Ext(req.URL.Path) filename = filename[0 : len(filename)-len(req_ext)] id := b62.Decode(filename) - 10000 var img *Image var err error if werr := useAPI(func(api *API) *web.Error { img, err = api.GetImage(id) if err != nil { return web.AsError(err, http.StatusInternalServerError) } if img == nil { return web.AsError(fmt.Errorf("No record for ID exists"), http.StatusNotFound) } // Follow redirect chain: for img.RedirectToID != nil { newimg, err := api.GetImage(*img.RedirectToID) if err != nil { return web.AsError(err, http.StatusInternalServerError) } img = newimg } return nil }); werr != nil { return werr.AsHTML() } // Determine mime-type and file extension: if img.Kind == "" { img.Kind = "gif" } mime, _, thumbExt := imageKindTo(img.Kind) // Find the image file: img_name := strconv.FormatInt(img.ID, 10) if dir == "/b" || dir == "/w" || dir == "/g" { // Render a black or white BG centered image viewer: var bgcolor string switch dir { case "/b": bgcolor = "black" case "/w": bgcolor = "white" case "/g": bgcolor = "gray" } model := viewTemplateModel{ BGColor: bgcolor, Query: flattenQuery(req_query), Image: *xlatImageViewModel(img, nil), } rsp.Header().Set("Content-Type", "text/html; charset=utf-8") if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "view", model), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil } else if dir == "/t" { // Serve thumbnail file: local_path := path.Join(store_folder(), img_name+req_ext) thumb_path := path.Join(thumb_folder(), img_name+thumbExt) if werr := web.AsError(ensureThumbnail(local_path, thumb_path), http.StatusInternalServerError); werr != nil { runtime.GC() return werr.AsHTML() } if xrThumb != "" { // Pass request to nginx to serve static content file: redirPath := path.Join(xrThumb, img_name+thumbExt) rsp.Header().Set("X-Accel-Redirect", redirPath) rsp.Header().Set("Content-Type", mime) rsp.WriteHeader(200) runtime.GC() return nil } else { rsp.Header().Set("Content-Type", mime) http.ServeFile(rsp, req, thumb_path) runtime.GC() return nil } } // Serve actual image contents: if xrGif != "" { // Pass request to nginx to serve static content file: redirPath := path.Join(xrGif, img_name+req_ext) rsp.Header().Set("X-Accel-Redirect", redirPath) rsp.Header().Set("Content-Type", mime) rsp.WriteHeader(200) runtime.GC() return nil } else { // Serve content directly with the proper mime-type: local_path := path.Join(store_folder(), img_name+req_ext) rsp.Header().Set("Content-Type", mime) http.ServeFile(rsp, req, local_path) runtime.GC() return nil } }
func downloadImageFor(store *imageStoreRequest) *web.Error { // Validate the URL: imgurl, err := url.Parse(store.SourceURL) if err != nil { return web.AsError(err, http.StatusBadRequest) } fetchurl := store.SourceURL ext := filepath.Ext(imgurl.Path) // Check if it's a youtube link: if (imgurl.Scheme == "http" || imgurl.Scheme == "https") && (imgurl.Host == "www.youtube.com") { // Process youtube links specially: if imgurl.Path != "/watch" { return web.AsError(fmt.Errorf("Unrecognized YouTube URL form."), http.StatusBadRequest) } store.Kind = "youtube" store.SourceURL = imgurl.Query().Get("v") return nil } // Check for imgur's gifv format: if (imgurl.Host == "imgur.com" || imgurl.Host == "i.imgur.com") && (ext == ".gifv" || ext == ".webm" || ext == ".mp4") { store.Kind = "imgur-gifv" _, fname := filepath.Split(imgurl.Path) store.SourceURL = filename(fname) } if imgurl.Host == "imgur.com" && imgurl.Path[0:9] == "/gallery/" { store.Kind = "imgur-gifv" _, fname := filepath.Split(imgurl.Path) store.SourceURL = filename(fname) } if store.Kind == "imgur-gifv" { // Background-fetch the WEBM and MP4 files: wg := &sync.WaitGroup{} fetch_func := func(ext string, path *string) { defer wg.Done() // Fetch the GIF version: var werr *web.Error *path, werr = downloadFile("http://i.imgur.com/" + store.SourceURL + ext) if werr != nil { log.Println(werr) return } } var ( webm_file string mp4_file string ) wg.Add(2) go fetch_func(".webm", &webm_file) go fetch_func(".mp4", &mp4_file) // Function to run after DB record creation: store.PostCreation = func(id int64, newImage *Image) (werr *web.Error) { // Wait for all files to download: wg.Wait() // Move temp files to final storage: if werr = moveToStoreFolder(webm_file, id, ".webm"); werr != nil { return } if werr = moveToStoreFolder(mp4_file, id, ".mp4"); werr != nil { return } return nil } return nil } // Do a HTTP GET to fetch the image: local_path, werr := downloadFile(fetchurl) if werr != nil { return werr } // Function to run after DB record creation: store.PostCreation = func(id int64, newImage *Image) (werr *web.Error) { return moveFiles(local_path, id, newImage) } return nil }
func processProxiedRequest(rsp http.ResponseWriter, req *http.Request, u *url.URL) *web.Error { relPath := removeIfStartsWith(u.Path, proxyRoot) localPath := path.Join(jailRoot, relPath) // Check if the requested path is a symlink: fi, err := os.Lstat(localPath) if fi != nil && (fi.Mode()&os.ModeSymlink) != 0 { localDir := path.Dir(localPath) // Check if file is a symlink and do 302 redirect: linkDest, err := os.Readlink(localPath) if err != nil { return web.AsError(err, http.StatusBadRequest) } // NOTE(jsd): Problem here for links outside the jail folder. if path.IsAbs(linkDest) && !strings.HasPrefix(linkDest, jailRoot) { return web.AsError(errors.New("Symlink points outside of jail"), http.StatusBadRequest) } linkDest = path.Join(localDir, linkDest) tp := translateForProxy(linkDest) doRedirect(req, rsp, tp, http.StatusFound) return nil } // Regular stat fi, err = os.Stat(localPath) if err != nil { return web.AsError(err, http.StatusNotFound) } // Serve the file if it is regular: if fi.Mode().IsRegular() { // Send file: // NOTE(jsd): using `http.ServeFile` does not appear to handle range requests well. Lots of broken pipe errors // that lead to a poor client experience. X-Accel-Redirect back to nginx is much better. if accelRedirect != "" { // Use X-Accel-Redirect if the cmdline option was given: redirPath := path.Join(accelRedirect, relPath) rsp.Header().Add("X-Accel-Redirect", redirPath) rsp.Header().Add("Content-Type", mime.TypeByExtension(path.Ext(localPath))) rsp.WriteHeader(200) } else { // Just serve the file directly from the filesystem: http.ServeFile(rsp, req, localPath) } return nil } // Generate an index.html for directories: if fi.Mode().IsDir() { dl := u.Query().Get("dl") if dl != "" { switch dl { default: fallthrough case "zip": downloadZip(rsp, req, u, &fi, localPath) //case "tar": // downloadTar(rsp, req, u, &fi, localPath) } return nil } return generateIndexHtml(rsp, req, u) } return nil }
func generateIndexHtml(rsp http.ResponseWriter, req *http.Request, u *url.URL) *web.Error { // Build index.html relPath := removeIfStartsWith(u.Path, proxyRoot) localPath := path.Join(jailRoot, relPath) pathLink := path.Join(proxyRoot, relPath) baseDir := path.Dir(localPath) if localPath[len(localPath)-1] == '/' { baseDir = path.Dir(localPath[0 : len(localPath)-1]) } if baseDir == "" { baseDir = "/" } // Determine what mode to sort by... sortString := "" // Check the .index-sort file: if sf, err := os.Open(path.Join(localPath, ".index-sort")); err == nil { defer sf.Close() scanner := bufio.NewScanner(sf) if scanner.Scan() { sortString = scanner.Text() } } // Use query-string 'sort' to override sorting: sortStringQuery := u.Query().Get("sort") if sortStringQuery != "" { sortString = sortStringQuery } // default Sort mode for headers nameSort := "name-asc" dateSort := "date-asc" sizeSort := "size-asc" // Determine the sorting mode: sortBy, sortDir := sortByName, sortAscending switch sortString { case "size-desc": sortBy, sortDir = sortBySize, sortDescending case "size-asc": sortBy, sortDir = sortBySize, sortAscending sizeSort = "size-desc" case "date-desc": sortBy, sortDir = sortByDate, sortDescending case "date-asc": sortBy, sortDir = sortByDate, sortAscending dateSort = "date-desc" case "name-desc": sortBy, sortDir = sortByName, sortDescending case "name-asc": sortBy, sortDir = sortByName, sortAscending nameSort = "name-desc" default: } // Open the directory to read its contents: f, err := os.Open(localPath) if err != nil { return web.AsError(err, http.StatusInternalServerError) } defer f.Close() // Read the directory entries: fis, err := f.Readdir(0) if err != nil { return web.AsError(err, http.StatusInternalServerError) } // Sort the entries by the desired mode: switch sortBy { default: sort.Sort(ByName{fis, sortDir}) case sortByName: sort.Sort(ByName{fis, sortDir}) case sortByDate: sort.Sort(ByDate{fis, sortDir}) case sortBySize: sort.Sort(BySize{fis, sortDir}) } files := make([]*IndexTemplateFile, 0, len(fis)) audioFiles := make([]*IndexTemplateAudioFileJSON, 0, len(fis)) // Check if there are MP3s in this directory: hasMP3s := false i := 0 for _, dfi := range fis { name := dfi.Name() // No hidden files: if name[0] == '.' { continue } dfi = followSymlink(localPath, dfi) dfiPath := path.Join(localPath, name) href := translateForProxy(dfiPath) ext := path.Ext(name) onlyname := name if ext != "" { onlyname = name[0 : len(name)-len(ext)] } mt := mime.TypeByExtension(path.Ext(dfi.Name())) sizeText := "" if dfi.IsDir() { sizeText = "-" name += "/" href += "/" } else { size := dfi.Size() if size < 1024 { sizeText = fmt.Sprintf("%d B", size) } else if size < 1024*1024 { sizeText = fmt.Sprintf("%.02f KB", float64(size)/1024.0) } else if size < 1024*1024*1024 { sizeText = fmt.Sprintf("%.02f MB", float64(size)/(1024.0*1024.0)) } else { sizeText = fmt.Sprintf("%.02f GB", float64(size)/(1024.0*1024.0*1024.0)) } } file := &IndexTemplateFile{ Href: href, Name: name, NameOnly: onlyname, Date: dfi.ModTime().Format("2006-01-02 15:04:05 -0700 MST"), SizeHumanReadable: template.HTML(strings.Replace(html.EscapeString(sizeText), " ", " ", -1)), MimeType: mt, IsFolder: dfi.IsDir(), } files = append(files, file) if !dfi.IsDir() && isMP3(dfi.Name()) { hasMP3s = true file.IsAudio = true audioFiles = append(audioFiles, &IndexTemplateAudioFileJSON{ Href: href, Name: onlyname, Index: i, }) i++ } } // TODO: determine how to handle this; in template or in code? if !useJPlayer { hasMP3s = false } audioFilesJSON, err := json.Marshal(audioFiles) if err != nil { return web.AsError(err, http.StatusInternalServerError) } templateData := &IndexTemplate{ JplayerUrl: jplayerUrl, Path: pathLink, HasAudio: hasMP3s, SortName: nameSort, SortDate: dateSort, SortSize: sizeSort, Files: files, AudioFiles: template.JS(audioFilesJSON), HasParent: strings.HasPrefix(baseDir, jailRoot), ParentHref: endSlash(translateForProxy(baseDir)), } // TODO: check Accepts header to reply accordingly (i.e. add JSON support) // Render index template: rsp.Header().Set("Content-Type", "text/html; charset=utf-8") rsp.WriteHeader(200) if werr := web.AsError(uiTmpl.ExecuteTemplate(rsp, "index", templateData), http.StatusInternalServerError); werr != nil { return werr.AsHTML() } return nil }