diff --git a/.env.example b/.env.example deleted file mode 100644 index fba2c3ea..00000000 --- a/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -DB_PATH="./tubely.db" -JWT_SECRET="JKFNDKAJSDKFASFNJWIROIOTNKNFDSKNFD" -PLATFORM="dev" -FILEPATH_ROOT="./app" -ASSETS_ROOT="./assets" -S3_BUCKET="tubely-123456789" -S3_REGION="us-east-2" -S3_CF_DISTRO="TEST" -PORT="8091" -# aws credentials should be set in ~/.aws/credentials -# using the `aws configure` command, the SDK will automatically -# read them from there diff --git a/README.md b/README.md deleted file mode 100644 index 34d955bc..00000000 --- a/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# learn-file-storage-s3-golang-starter (Tubely) - -This repo contains the starter code for the Tubely application - the #1 tool for engagement bait - for the "Learn File Servers and CDNs with S3 and CloudFront" [course](https://www.boot.dev/courses/learn-file-servers-s3-cloudfront-golang) on [boot.dev](https://www.boot.dev) - -## Quickstart - -*This is to be used as a *reference\* in case you need it, you should follow the instructions in the course rather than trying to do everything here. - -## 1. Install dependencies - -- [Go](https://golang.org/doc/install) -- `go mod download` to download all dependencies -- [FFMPEG](https://ffmpeg.org/download.html) - both `ffmpeg` and `ffprobe` are required to be in your `PATH`. - -```bash -# linux -sudo apt update -sudo apt install ffmpeg - -# mac -brew update -brew install ffmpeg -``` - -- [SQLite 3](https://www.sqlite.org/download.html) only required for you to manually inspect the database. - -```bash -# linux -sudo apt update -sudo apt install sqlite3 - -# mac -brew update -brew install sqlite3 -``` - -- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) - -## 2. Download sample images and videos - -```bash -./samplesdownload.sh -# samples/ dir will be created -# with sample images and videos -``` - -## 3. Configure environment variables - -Copy the `.env.example` file to `.env` and fill in the values. - -```bash -cp .env.example .env -``` - -You'll need to update values in the `.env` file to match your configuration, but _you won't need to do anything here until the course tells you to_. - -## 3. Run the server - -```bash -go run . -``` - -- You should see a new database file `tubely.db` created in the root directory. -- You should see a new `assets` directory created in the root directory, this is where the images will be stored. -- You should see a link in your console to open the local web page. diff --git a/app/app.js b/app/app.js index f688af96..50cf6f85 100644 --- a/app/app.js +++ b/app/app.js @@ -261,7 +261,7 @@ function viewVideo(video) { thumbnailImg.style.display = 'none'; } else { thumbnailImg.style.display = 'block'; - thumbnailImg.src = video.thumbnail_url; + thumbnailImg.src = `${video.thumbnail_url}?v=${Date.now()}`; } const videoPlayer = document.getElementById('video-player'); diff --git a/assets.go b/assets.go index 8315787d..c7de63fb 100644 --- a/assets.go +++ b/assets.go @@ -1,7 +1,12 @@ package main import ( + "crypto/rand" + "encoding/base64" + "fmt" "os" + "path/filepath" + "strings" ) func (cfg apiConfig) ensureAssetsDir() error { @@ -10,3 +15,32 @@ func (cfg apiConfig) ensureAssetsDir() error { } return nil } + +func getAssetPath(mediaType string) string { + base := make([]byte, 32) + _, err := rand.Read(base) + if err != nil { + panic("failed to generate video id") + } + + videoID := base64.RawURLEncoding.EncodeToString(base) + + ext := mediaTypeToExt(mediaType) + return fmt.Sprintf("%s%s", videoID, ext) +} + +func (cfg apiConfig) getAssetDiskPath(assetPath string) string { + return filepath.Join(cfg.assetsRoot, assetPath) +} + +func (cfg apiConfig) getAssetURL(assetPath string) string { + return fmt.Sprintf("http://localhost:%s/assets/%s", cfg.port, assetPath) +} + +func mediaTypeToExt(mediaType string) string { + parts := strings.Split(mediaType, "/") + if len(parts) != 2 { + return ".bin" + } + return "." + parts[1] +} diff --git a/cache.go b/cache.go index 84ac0afb..6cdfaf48 100644 --- a/cache.go +++ b/cache.go @@ -2,9 +2,9 @@ package main import "net/http" -func cacheMiddleware(next http.Handler) http.Handler { +func noCacheMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "max-age=3600") + w.Header().Set("Cache-Control", "no-store") next.ServeHTTP(w, r) }) } diff --git a/go.mod b/go.mod index f8ae5007..af3d58d6 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/bootdotdev/learn-file-storage-s3-golang-starter +module github.com/imhasandl/learn-file-storage-s3-golang-starter go 1.23.0 diff --git a/handler_get_thumbnail.go b/handler_get_thumbnail.go deleted file mode 100644 index 1ddac141..00000000 --- a/handler_get_thumbnail.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - - "github.com/google/uuid" -) - -func (cfg *apiConfig) handlerThumbnailGet(w http.ResponseWriter, r *http.Request) { - videoIDString := r.PathValue("videoID") - videoID, err := uuid.Parse(videoIDString) - if err != nil { - respondWithError(w, http.StatusBadRequest, "Invalid video ID", err) - return - } - - tn, ok := videoThumbnails[videoID] - if !ok { - respondWithError(w, http.StatusNotFound, "Thumbnail not found", nil) - return - } - - w.Header().Set("Content-Type", tn.mediaType) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(tn.data))) - - _, err = w.Write(tn.data) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Error writing response", err) - return - } -} diff --git a/handler_login.go b/handler_login.go index 9ccf9f74..92d858c6 100644 --- a/handler_login.go +++ b/handler_login.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/auth" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/database" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/auth" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/database" ) func (cfg *apiConfig) handlerLogin(w http.ResponseWriter, r *http.Request) { diff --git a/handler_refresh.go b/handler_refresh.go index 029f22ce..eefc36ea 100644 --- a/handler_refresh.go +++ b/handler_refresh.go @@ -4,7 +4,7 @@ import ( "net/http" "time" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/auth" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/auth" ) func (cfg *apiConfig) handlerRefresh(w http.ResponseWriter, r *http.Request) { diff --git a/handler_upload_thumbnail.go b/handler_upload_thumbnail.go index 765d87d3..4b0b6e21 100644 --- a/handler_upload_thumbnail.go +++ b/handler_upload_thumbnail.go @@ -1,10 +1,12 @@ package main import ( - "fmt" + "io" + "mime" "net/http" + "os" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/auth" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/auth" "github.com/google/uuid" ) @@ -28,10 +30,57 @@ func (cfg *apiConfig) handlerUploadThumbnail(w http.ResponseWriter, r *http.Requ return } + const maxMemory = 10 << 20 // 10 MB + r.ParseMultipartForm(maxMemory) - fmt.Println("uploading thumbnail for video", videoID, "by user", userID) + file, header, err := r.FormFile("thumbnail") + if err != nil { + respondWithError(w, http.StatusBadRequest, "Unable to parse form file", err) + return + } + defer file.Close() - // TODO: implement the upload here + mediaType, _, err := mime.ParseMediaType(header.Header.Get("Content-Type")) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid Content-Type", err) + return + } + if mediaType != "image/jpeg" && mediaType != "image/png" { + respondWithError(w, http.StatusBadRequest, "Invalid file type", nil) + return + } + + assetPath := getAssetPath(mediaType) + assetDiskPath := cfg.getAssetDiskPath(assetPath) + + dst, err := os.Create(assetDiskPath) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Unable to create file on server", err) + return + } + defer dst.Close() + if _, err = io.Copy(dst, file); err != nil { + respondWithError(w, http.StatusInternalServerError, "Error saving file", err) + return + } + + video, err := cfg.db.GetVideo(videoID) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't find video", err) + return + } + if video.UserID != userID { + respondWithError(w, http.StatusUnauthorized, "Not authorized to update this video", nil) + return + } + + url := cfg.getAssetURL(assetPath) + video.ThumbnailURL = &url + err = cfg.db.UpdateVideo(video) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't update video", err) + return + } - respondWithJSON(w, http.StatusOK, struct{}{}) + respondWithJSON(w, http.StatusOK, video) } diff --git a/handler_users.go b/handler_users.go index d25e6327..e5604f4e 100644 --- a/handler_users.go +++ b/handler_users.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/auth" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/database" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/auth" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/database" ) func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { diff --git a/handler_video_meta.go b/handler_video_meta.go index 32660382..56fee163 100644 --- a/handler_video_meta.go +++ b/handler_video_meta.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/auth" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/database" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/auth" + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/database" "github.com/google/uuid" ) diff --git a/internal/database/users.go b/internal/database/users.go index 4f162240..9377a16d 100644 --- a/internal/database/users.go +++ b/internal/database/users.go @@ -17,7 +17,7 @@ type User struct { type CreateUserParams struct { Email string `json:"email"` - Password string `json:"password"` + Password string `json:"-"` } func (c Client) GetUsers() ([]User, error) { diff --git a/main.go b/main.go index a48f1405..a03a0c9c 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,7 @@ import ( "net/http" "os" - "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/database" - "github.com/google/uuid" - + "github.com/imhasandl/learn-file-storage-s3-golang-starter/internal/database" "github.com/joho/godotenv" _ "github.com/lib/pq" ) @@ -24,13 +22,6 @@ type apiConfig struct { port string } -type thumbnail struct { - data []byte - mediaType string -} - -var videoThumbnails = map[uuid.UUID]thumbnail{} - func main() { godotenv.Load(".env") @@ -106,11 +97,11 @@ func main() { mux.Handle("/app/", appHandler) assetsHandler := http.StripPrefix("/assets", http.FileServer(http.Dir(assetsRoot))) - mux.Handle("/assets/", cacheMiddleware(assetsHandler)) + mux.Handle("/assets/", noCacheMiddleware(assetsHandler)) mux.HandleFunc("POST /api/login", cfg.handlerLogin) mux.HandleFunc("POST /api/refresh", cfg.handlerRefresh) - mux.HandleFunc("POST /api/revoke", cfg.handlerRevoke) + mux.HandleFunc("POScT /api/revoke", cfg.handlerRevoke) mux.HandleFunc("POST /api/users", cfg.handlerUsersCreate) @@ -119,7 +110,6 @@ func main() { mux.HandleFunc("POST /api/video_upload/{videoID}", cfg.handlerUploadVideo) mux.HandleFunc("GET /api/videos", cfg.handlerVideosRetrieve) mux.HandleFunc("GET /api/videos/{videoID}", cfg.handlerVideoGet) - mux.HandleFunc("GET /api/thumbnails/{videoID}", cfg.handlerThumbnailGet) mux.HandleFunc("DELETE /api/videos/{videoID}", cfg.handlerVideoMetaDelete) mux.HandleFunc("POST /admin/reset", cfg.handlerReset)