video upload to AWS
This commit is contained in:
21
go.mod
21
go.mod
@@ -13,3 +13,24 @@ require (
|
|||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
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
|
||||||
|
)
|
||||||
|
|||||||
36
go.sum
36
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 h1:tDQ1LjKga657layZ4JLsRdxgvupebc0xuPwRNuTfUgs=
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|||||||
@@ -1,7 +1,149 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
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"
|
"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)
|
||||||
|
}
|
||||||
|
|||||||
13
main.go
13
main.go
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -17,6 +20,7 @@ type apiConfig struct {
|
|||||||
platform string
|
platform string
|
||||||
filepathRoot string
|
filepathRoot string
|
||||||
assetsRoot string
|
assetsRoot string
|
||||||
|
s3Client *s3.Client
|
||||||
s3Bucket string
|
s3Bucket string
|
||||||
s3Region string
|
s3Region string
|
||||||
s3CfDistribution string
|
s3CfDistribution string
|
||||||
@@ -98,6 +102,15 @@ func main() {
|
|||||||
log.Fatalf("Couldn't create assets directory: %v", err)
|
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()
|
mux := http.NewServeMux()
|
||||||
appHandler := http.StripPrefix("/app", http.FileServer(http.Dir(filepathRoot)))
|
appHandler := http.StripPrefix("/app", http.FileServer(http.Dir(filepathRoot)))
|
||||||
mux.Handle("/app/", appHandler)
|
mux.Handle("/app/", appHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user