diff --git a/go.mod b/go.mod index f8ae500..3610d82 100644 --- a/go.mod +++ b/go.mod @@ -13,3 +13,24 @@ require ( github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.24 ) + +require ( + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.9 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect + github.com/aws/smithy-go v1.22.2 // indirect +) diff --git a/go.sum b/go.sum index 9903dbd..630b207 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,39 @@ +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= +github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/golang-jwt/jwt/v5 v5.0.0-rc.1 h1:tDQ1LjKga657layZ4JLsRdxgvupebc0xuPwRNuTfUgs= github.com/golang-jwt/jwt/v5 v5.0.0-rc.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/handler_upload_video.go b/handler_upload_video.go index 44e0549..7265c12 100644 --- a/handler_upload_video.go +++ b/handler_upload_video.go @@ -1,7 +1,149 @@ package main import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/bootdotdev/learn-file-storage-s3-golang-starter/internal/auth" + "github.com/google/uuid" + "io" + "mime" "net/http" + "os" ) -func (cfg *apiConfig) handlerUploadVideo(w http.ResponseWriter, r *http.Request) {} +func (cfg *apiConfig) handlerUploadVideo(w http.ResponseWriter, r *http.Request) { + videoIDString := r.PathValue("videoID") + videoID, err := uuid.Parse(videoIDString) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid ID", err) + return + } + + token, err := auth.GetBearerToken(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find JWT", err) + return + } + + userID, err := auth.ValidateJWT(token, cfg.jwtSecret) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't validate JWT", err) + return + } + + fmt.Println("uploading video", videoID, "by user", userID) + + var maxMemory int64 + maxMemory = 10 << 30 + + err = r.ParseMultipartForm(maxMemory) + + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't parse multipart form", err) + return + } + + videoFile, header, err := r.FormFile("video") + defer videoFile.Close() + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't get file", err) + return + } + + mediaType := header.Header.Get("Content-Type") + + videoData, err := io.ReadAll(videoFile) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't read video file", err) + return + } + + video, err := cfg.db.GetVideo(videoID) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't find video", err) + return + } + if video.UserID != userID { + respondWithError(w, http.StatusUnauthorized, "You are not authorized to upload this video", err) + return + } + + var extension string + + mimeType, _, err := mime.ParseMediaType(mediaType) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't parse multipart form", err) + return + } + + switch mimeType { + case "video/mp4": + extension = ".mp4" + break + default: + respondWithError(w, http.StatusBadRequest, "Couldn't recognize file type", err) + return + } + + videoName := make([]byte, 32) + _, err = rand.Read(videoName) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't generate random name", err) + return + } + + videoPathName := base64.RawURLEncoding.EncodeToString(videoName) + + tempFile, err := os.CreateTemp("", videoPathName) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't create temp file", err) + return + } + + defer os.Remove(videoPathName) + defer tempFile.Close() + + _, err = io.Copy(tempFile, bytes.NewReader(videoData)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create file", err) + return + } + + tempFile.Seek(0, io.SeekStart) + + fileName := videoPathName + extension + + _, err = cfg.s3Client.PutObject(context.Background(), &s3.PutObjectInput{ + Bucket: &cfg.s3Bucket, + Key: &fileName, + Body: tempFile, + ContentType: &mimeType, + }) + if err != nil { + fmt.Printf("%s region, %s bucket", cfg.s3Region, cfg.s3Bucket) + respondWithError(w, http.StatusInternalServerError, "Couldn't upload video", err) + return + } + + url := fmt.Sprintf("http://%s.s3.%s.amazonaws.com/%s", cfg.s3Bucket, cfg.s3Region, fileName) + + video.VideoURL = &url + + err = cfg.db.UpdateVideo(video) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't update video", err) + return + } + + video, err = cfg.db.GetVideo(videoID) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Couldn't find video", err) + return + } + + respondWithJSON(w, http.StatusOK, video) +} diff --git a/main.go b/main.go index f0cfec7..1dbc93d 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,9 @@ package main import ( + "context" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" "log" "net/http" "os" @@ -17,6 +20,7 @@ type apiConfig struct { platform string filepathRoot string assetsRoot string + s3Client *s3.Client s3Bucket string s3Region string s3CfDistribution string @@ -98,6 +102,15 @@ func main() { log.Fatalf("Couldn't create assets directory: %v", err) } + awsCfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(cfg.s3Region)) + if err != nil { + log.Fatal(err) + } + + client := s3.NewFromConfig(awsCfg) + + cfg.s3Client = client + mux := http.NewServeMux() appHandler := http.StripPrefix("/app", http.FileServer(http.Dir(filepathRoot))) mux.Handle("/app/", appHandler)