package hmns3 import ( "context" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "strings" "git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/jobs" "git.handmade.network/hmn/hmn/src/logging" "git.handmade.network/hmn/hmn/src/utils" "github.com/rs/zerolog" ) const dir = "./tmp/s3" type server struct { log zerolog.Logger } func StartServer(ctx context.Context) jobs.Job { if !config.Config.DigitalOcean.RunFakeServer { return jobs.Noop() } utils.Must0(os.MkdirAll(dir, fs.ModePerm)) s := server{ log: logging.ExtractLogger(ctx).With(). Str("module", "S3 server"). Logger(), } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { s.getObject(w, r) } else if r.Method == http.MethodPut { s.putObject(w, r) } else { panic("Unimplemented method!") } }) job := jobs.New() srv := http.Server{ Addr: config.Config.DigitalOcean.FakeAddr, } s.log.Info().Msg("Starting local S3 server") go func() { defer job.Done() err := srv.ListenAndServe() if err != nil { if errors.Is(err, http.ErrServerClosed) { // This is normal and fine } else { panic(err) } } }() go func() { <-ctx.Done() s.log.Info().Msg("Shutting down local S3 server") srv.Shutdown(context.Background()) }() return job } func (s *server) getObject(w http.ResponseWriter, r *http.Request) { bucket, key := bucketKey(r) file := utils.Must1(os.Open(filepath.Join(dir, bucket, key))) io.Copy(w, file) } func (s *server) putObject(w http.ResponseWriter, r *http.Request) { bucket, key := bucketKey(r) w.Header().Set("Location", fmt.Sprintf("/%s", bucket)) utils.Must0(os.MkdirAll(filepath.Join(dir, bucket), fs.ModePerm)) if key != "" { file := utils.Must1(os.Create(filepath.Join(dir, bucket, key))) io.Copy(file, r.Body) } } func bucketKey(r *http.Request) (string, string) { slashIdx := strings.IndexByte(r.URL.Path[1:], '/') if slashIdx == -1 { return r.URL.Path[1:], "" } else { return r.URL.Path[1 : 1+slashIdx], strings.ReplaceAll(r.URL.Path[2+slashIdx:], "/", "~") } }