diff --git a/go.mod b/go.mod index eddf7e2..355f286 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( forge.capytal.company/loreddev/x v0.0.0-20250305165122-0ccb26ab783b github.com/aws/aws-sdk-go-v2 v1.36.3 github.com/aws/aws-sdk-go-v2/service/s3 v1.78.1 - github.com/google/uuid v1.6.0 github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92 ) diff --git a/go.sum b/go.sum index 097a65e..c01df5f 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ 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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/internals/randstr/randstr.go b/internals/randstr/randstr.go new file mode 100644 index 0000000..358c04d --- /dev/null +++ b/internals/randstr/randstr.go @@ -0,0 +1,75 @@ +// This file has code copied from the "randstr" Go module, which can be found at +// https://github.com/thanhpk/randsr. The original code is licensed under the MIT +// license, which a copy can be found at https://github.com/thanhpk/randstr/blob/master/LICENSE +// and is provided below: +// +// # The MIT License +// +// Copyright (c) 2010-2018 Google, Inc. http://angularjs.org +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package randstr provides basic functions for generating random bytes, string +package randstr + +import ( + "bytes" + "crypto/rand" + "encoding/binary" +) + +// HexChars holds a string containing all characters used in a hexadecimal value. +const HexChars = "0123456789abcdef" + +// NewHex generates a new Hexadecimal string with length of n +// +// Example: 67aab2d956bd7cc621af22cfb169cba8 +func NewHex(n int) (string, error) { return New(n, HexChars) } + +// New generates a random string using only letters provided in the letters parameter. +// +// If the letters parameter is omitted, this function will use HexChars instead. +func New(n int, chars ...string) (string, error) { + runes := []rune(HexChars) + if len(chars) > 0 { + runes = []rune(chars[0]) + } + + var b bytes.Buffer + b.Grow(n) + l := uint32(len(runes)) + for range n { + by, err := Bytes(4) + if err != nil { + return "", err + } + b.WriteRune(runes[binary.BigEndian.Uint32(by)%l]) + } + return b.String(), nil +} + +// Bytes generates n random bytes +func Bytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return []byte{}, err + } + return b, nil +} diff --git a/service/projects.go b/service/projects.go index f5e9691..228dcc2 100644 --- a/service/projects.go +++ b/service/projects.go @@ -1,33 +1,118 @@ package service import ( + "bytes" "encoding/xml" "errors" + "fmt" + "io" + "log/slog" "forge.capytal.company/capytalcode/project-comicverse/database" + "forge.capytal.company/capytalcode/project-comicverse/internals/randstr" + "github.com/aws/aws-sdk-go-v2/service/s3" ) -func (s *service) NewProject() error { - s.assert.NotNil(s.db) +const projectIDLength = 6 - id, err := uuid.NewV7() +var ( + ErrProjectNotExists = errors.New("project does not exists in database") + ErrProjectInvalidUUID = errors.New("UUID provided is invalid") +) + +type Project struct { + XMLName xml.Name `xml:"body"` + ID string `xml:"id,attr"` + Title string `xml:"h1"` + Contents string `xml:"-"` +} + +func (s *Service) CreateProject() (Project, error) { + s.assert.NotNil(s.db) + s.assert.NotNil(s.s3) + s.assert.NotNil(s.ctx) + s.assert.NotZero(s.bucket) + + s.log.Debug("Creating new project") + + id, err := randstr.NewHex(projectIDLength) if err != nil { - return err + return Project{}, err } - s.assert.NotZero(id.String(), "UUID should never be invalid") + title := "New Project" - err = s.db.Insert(&database.Project{ - ID: id.String(), - Title: "New Project", + s.assert.NotZero(id, "ID should never be empty") + + s.log.Debug("Creating project on database", slog.String("id", id)) + + _, err = s.db.CreateProject(id, title) + if err != nil { + return Project{}, err + } + + p := Project{ + ID: id, + Title: title, + } + + c, err := xml.Marshal(p) + if err != nil { + return Project{}, err + } + + s.log.Debug("Creating project on storage", slog.String("id", id)) + + f := fmt.Sprintf("%s.comic.xml", id) + _, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{ + Bucket: &s.bucket, + Key: &f, + Body: bytes.NewReader(c), }) if err != nil { - return errors.Join(errors.New("database returned error while inserting new project"), err) + return Project{}, err } + p.Contents = string(c) - return nil + return p, nil } -func (s *service) ListProjects() { +func (s *Service) GetProject(id string) (Project, error) { + s.assert.NotNil(s.db) + s.assert.NotNil(s.s3) + s.assert.NotZero(s.bucket) + s.assert.NotNil(s.ctx) + s.assert.NotZero(id) + + res, err := s.db.GetProject(id) + if errors.Is(err, database.ErrNoRows) { + return Project{}, errors.Join(ErrProjectNotExists, err) + } + if err != nil { + return Project{}, err + } + + p := Project{ + ID: res.ID, + Title: res.Title, + } + + f := fmt.Sprintf("%s.comic.xml", p.ID) + file, err := s.s3.GetObject(s.ctx, &s3.GetObjectInput{ + Bucket: &s.bucket, + Key: &f, + }) + if err != nil { + return p, err + } + + c, err := io.ReadAll(file.Body) + if err != nil { + return p, err + } + + p.Contents = string(c) + + return p, nil }