Beispiel #1
0
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
}
Beispiel #2
0
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
}
Beispiel #3
0
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
}
Beispiel #4
0
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)
}
Beispiel #5
0
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
}
Beispiel #6
0
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
}
Beispiel #7
0
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
}
Beispiel #8
0
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
}
Beispiel #9
0
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
}
Beispiel #10
0
// 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
}
Beispiel #11
0
// 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
	}
}
Beispiel #12
0
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
}
Beispiel #13
0
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
}
Beispiel #14
0
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), " ", "&nbsp;", -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
}