diff options
| -rw-r--r-- | cmd/xedn/uplodr/generate.go | 5 | ||||
| -rw-r--r-- | cmd/xedn/uplodr/main.go | 307 | ||||
| -rw-r--r-- | cmd/xedn/uplodr/pb/uplodr.pb.go | 379 | ||||
| -rw-r--r-- | cmd/xedn/uplodr/pb/uplodr_grpc.pb.go | 146 | ||||
| -rw-r--r-- | cmd/xedn/uplodr/uplodr.proto | 27 | ||||
| -rw-r--r-- | internal/tigris.go | 21 |
6 files changed, 885 insertions, 0 deletions
diff --git a/cmd/xedn/uplodr/generate.go b/cmd/xedn/uplodr/generate.go new file mode 100644 index 0000000..207e2c9 --- /dev/null +++ b/cmd/xedn/uplodr/generate.go @@ -0,0 +1,5 @@ +package main + +func init() {} + +//go:generate protoc --proto_path=. --go_out=./pb --go_opt=paths=source_relative --go-grpc_out=./pb --go-grpc_opt=paths=source_relative uplodr.proto diff --git a/cmd/xedn/uplodr/main.go b/cmd/xedn/uplodr/main.go new file mode 100644 index 0000000..bfca3aa --- /dev/null +++ b/cmd/xedn/uplodr/main.go @@ -0,0 +1,307 @@ +package main + +import ( + "bytes" + "context" + "errors" + "flag" + "fmt" + "image" + "image/jpeg" + "image/png" + "log" + "log/slog" + "net" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/chai2010/webp" + "github.com/disintegration/imaging" + "google.golang.org/grpc" + "within.website/x/cmd/xedn/uplodr/pb" + "within.website/x/internal" + "within.website/x/internal/avif" +) + +var ( + grpcAddr = flag.String("grpc-addr", ":8080", "address to listen on for GRPC") + + b2Bucket = flag.String("b2-bucket", "christine-static", "Backblaze B2 bucket to dump things to") + b2KeyID = flag.String("b2-key-id", "", "Backblaze B2 application key ID") + b2KeySecret = flag.String("b2-application-key", "", "Backblaze B2 application secret") + + tigrisBucket = flag.String("bucket-name", "xedn", "Tigris bucket to dump things to") + + avifQuality = flag.Int("avif-quality", 8, "AVIF quality (higher is worse quality)") + avifEncoderSpeed = flag.Int("avif-encoder-speed", 0, "AVIF encoder speed (higher is faster)") + + jpegQuality = flag.Int("jpeg-quality", 90, "JPEG quality (lower means lower file size)") + + webpQuality = flag.Int("webp-quality", 9, "WEBP quality (higher is worse quality)") +) + +func main() { + internal.HandleStartup() + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) + defer cancel() + + s, err := New(ctx) + if err != nil { + log.Fatal(err) + } + + go func() { + <-ctx.Done() + panic("timeout") + }() + + lis, err := net.Listen("tcp", *grpcAddr) + if err != nil { + log.Fatal(err) + } + + gs := grpc.NewServer() + + pb.RegisterImageServer(gs, s) + + log.Fatal(gs.Serve(lis)) +} + +type Server struct { + tc *s3.Client + b2c *s3.Client + + pb.UnimplementedImageServer +} + +func New(ctx context.Context) (*Server, error) { + tc, err := mkTigrisClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create Tigris client: %w", err) + } + + b2c := mkB2Client() + + return &Server{ + tc: tc, + b2c: b2c, + }, nil +} + +func (s *Server) Ping(ctx context.Context, msg *pb.Echo) (*pb.Echo, error) { + return msg, nil +} + +func (s *Server) Upload(ctx context.Context, ur *pb.UploadReq) (*pb.UploadResp, error) { + img, format, err := image.Decode(bytes.NewBuffer(ur.Data)) + if err != nil { + slog.Error("can't decode image", "err", err, "filename", ur.GetFileName()) + return nil, err + } + slog.Debug("got image", "format", format) + + baseName := fileNameWithoutExt(ur.GetFileName()) + + fnames := []string{} + + dir, err := os.MkdirTemp("", "xedn-uplodr") + if err != nil { + return nil, fmt.Errorf("failed to create temp dir: %w") + } + defer os.RemoveAll(dir) + + name := baseName + "-smol.png" + fnames = append(fnames, name) + if err := resizeSmol(img, filepath.Join(dir, name)); err != nil { + return nil, fmt.Errorf("failed to make smol png: %w", err) + } + + name = baseName + ".webp" + fnames = append(fnames, filepath.Join(dir, name)) + if err := doWEBP(img, name); err != nil { + return nil, fmt.Errorf("failed to make webp: %w", err) + } + + name = baseName + ".avif" + fnames = append(fnames, filepath.Join(dir, name)) + if err := doAVIF(img, name); err != nil { + return nil, fmt.Errorf("failed to make avif: %w", err) + } + + name = baseName + ".jpg" + fnames = append(fnames, filepath.Join(dir, name)) + if err := doJPEG(img, name); err != nil { + return nil, fmt.Errorf("failed to make jpeg: %w", err) + } + + var result []*pb.Variant + + errs := []error{} + for _, fname := range fnames { + path := filepath.Join(dir, fname) + slog.Info("uploading", "path", path) + + fin, err := os.Open(path) + if err != nil { + slog.Error("can't open file", "path", path, "err", err) + errs = append(errs, fmt.Errorf("while uploading %s: %w", path, err)) + continue + } + defer fin.Close() + + if _, err := s.b2c.PutObject(ctx, &s3.PutObjectInput{ + Body: fin, + Bucket: b2Bucket, + Key: aws.String(fmt.Sprintf("%s/%s", ur.Folder, fname)), + ContentType: aws.String(mimeTypes[filepath.Ext(fname)]), + }); err != nil { + slog.Error("can't upload", "to", "b2", "err", err) + errs = append(errs, fmt.Errorf("while uploading %s to b2: %w", path, err)) + continue + } + + fin.Seek(0, 0) + + if _, err := s.tc.PutObject(ctx, &s3.PutObjectInput{ + Body: fin, + Bucket: b2Bucket, + Key: aws.String(fmt.Sprintf("%s/%s", ur.Folder, fname)), + ContentType: aws.String(mimeTypes[filepath.Ext(fname)]), + }); err != nil { + slog.Error("can't upload", "to", "b2", "err", err) + errs = append(errs, fmt.Errorf("while uploading %s to b2: %w", path, err)) + continue + } + + result = append(result, &pb.Variant{ + Url: fmt.Sprintf("https://cdn.xeiaso.net/file/christine-static/%s/%s", ur.GetFolder(), fname), + MimeType: mimeTypes[filepath.Ext(fname)], + }) + } + + if len(errs) != 0 { + return nil, errors.Join(errs...) + } + + return &pb.UploadResp{ + Variants: result, + }, nil +} + +func doAVIF(src image.Image, dstPath string) error { + dst, err := os.Create(dstPath) + if err != nil { + log.Fatalf("Can't create destination file: %v", err) + } + defer dst.Close() + + err = avif.Encode(dst, src, &avif.Options{ + Threads: runtime.GOMAXPROCS(0), + Speed: *avifEncoderSpeed, + Quality: *avifQuality, + }) + if err != nil { + return err + } + + log.Printf("Encoded AVIF at %s", dstPath) + + return nil +} + +func doWEBP(src image.Image, dstPath string) error { + fout, err := os.Create(dstPath) + if err != nil { + return err + } + defer fout.Close() + + err = webp.Encode(fout, src, &webp.Options{Quality: float32(*webpQuality)}) + if err != nil { + return err + } + + log.Printf("Encoded WEBP at %s", dstPath) + + return nil +} + +func fileNameWithoutExt(fileName string) string { + return filepath.Base(fileName[:len(fileName)-len(filepath.Ext(fileName))]) +} + +func doJPEG(src image.Image, dstPath string) error { + fout, err := os.Create(dstPath) + if err != nil { + return err + } + defer fout.Close() + + if err := jpeg.Encode(fout, src, &jpeg.Options{Quality: *jpegQuality}); err != nil { + return err + } + + log.Printf("Encoded JPEG at %s", dstPath) + + return nil +} + +func resizeSmol(src image.Image, dstPath string) error { + fout, err := os.Create(dstPath) + if err != nil { + return err + } + defer fout.Close() + + dstImg := imaging.Resize(src, 800, 0, imaging.Lanczos) + + enc := png.Encoder{ + CompressionLevel: png.BestCompression, + } + + if err := enc.Encode(fout, dstImg); err != nil { + return err + } + + log.Printf("Encoded smol PNG at %s", dstPath) + + return nil +} + +var mimeTypes = map[string]string{ + ".avif": "image/avif", + ".webp": "image/webp", + ".jpg": "image/jpeg", + ".png": "image/png", + ".wasm": "application/wasm", + ".css": "text/css", +} + +func mkTigrisClient(ctx context.Context) (*s3.Client, error) { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load Tigris config: %w", err) + } + + return s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev") + }), nil +} + +func mkB2Client() *s3.Client { + s3Config := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider(*b2KeyID, *b2KeySecret, ""), + BaseEndpoint: aws.String("https://s3.us-west-001.backblazeb2.com"), + Region: "us-west-001", + } + s3Client := s3.NewFromConfig(s3Config, (func(o *s3.Options) { + o.UsePathStyle = true + })) + return s3Client +} diff --git a/cmd/xedn/uplodr/pb/uplodr.pb.go b/cmd/xedn/uplodr/pb/uplodr.pb.go new file mode 100644 index 0000000..b496363 --- /dev/null +++ b/cmd/xedn/uplodr/pb/uplodr.pb.go @@ -0,0 +1,379 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.23.4 +// source: uplodr.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type UploadReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Folder string `protobuf:"bytes,3,opt,name=folder,proto3" json:"folder,omitempty"` +} + +func (x *UploadReq) Reset() { + *x = UploadReq{} + if protoimpl.UnsafeEnabled { + mi := &file_uplodr_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UploadReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadReq) ProtoMessage() {} + +func (x *UploadReq) ProtoReflect() protoreflect.Message { + mi := &file_uplodr_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadReq.ProtoReflect.Descriptor instead. +func (*UploadReq) Descriptor() ([]byte, []int) { + return file_uplodr_proto_rawDescGZIP(), []int{0} +} + +func (x *UploadReq) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *UploadReq) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *UploadReq) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +type UploadResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Variants []*Variant `protobuf:"bytes,1,rep,name=variants,proto3" json:"variants,omitempty"` +} + +func (x *UploadResp) Reset() { + *x = UploadResp{} + if protoimpl.UnsafeEnabled { + mi := &file_uplodr_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UploadResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadResp) ProtoMessage() {} + +func (x *UploadResp) ProtoReflect() protoreflect.Message { + mi := &file_uplodr_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadResp.ProtoReflect.Descriptor instead. +func (*UploadResp) Descriptor() ([]byte, []int) { + return file_uplodr_proto_rawDescGZIP(), []int{1} +} + +func (x *UploadResp) GetVariants() []*Variant { + if x != nil { + return x.Variants + } + return nil +} + +type Variant struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + MimeType string `protobuf:"bytes,2,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` +} + +func (x *Variant) Reset() { + *x = Variant{} + if protoimpl.UnsafeEnabled { + mi := &file_uplodr_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Variant) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Variant) ProtoMessage() {} + +func (x *Variant) ProtoReflect() protoreflect.Message { + mi := &file_uplodr_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Variant.ProtoReflect.Descriptor instead. +func (*Variant) Descriptor() ([]byte, []int) { + return file_uplodr_proto_rawDescGZIP(), []int{2} +} + +func (x *Variant) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *Variant) GetMimeType() string { + if x != nil { + return x.MimeType + } + return "" +} + +type Echo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Nonce string `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"` +} + +func (x *Echo) Reset() { + *x = Echo{} + if protoimpl.UnsafeEnabled { + mi := &file_uplodr_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Echo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Echo) ProtoMessage() {} + +func (x *Echo) ProtoReflect() protoreflect.Message { + mi := &file_uplodr_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Echo.ProtoReflect.Descriptor instead. +func (*Echo) Descriptor() ([]byte, []int) { + return file_uplodr_proto_rawDescGZIP(), []int{3} +} + +func (x *Echo) GetNonce() string { + if x != nil { + return x.Nonce + } + return "" +} + +var File_uplodr_proto protoreflect.FileDescriptor + +var file_uplodr_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, + 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, + 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x22, 0x54, 0x0a, 0x09, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, + 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x22, 0x4f, 0x0a, 0x0a, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x41, 0x0a, 0x08, 0x76, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, + 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, + 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x52, 0x08, 0x76, 0x61, 0x72, 0x69, 0x61, + 0x6e, 0x74, 0x73, 0x22, 0x38, 0x0a, 0x07, 0x56, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, + 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x22, 0x1c, 0x0a, + 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x32, 0xb4, 0x01, 0x0a, 0x05, + 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x4e, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x22, 0x2e, + 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, + 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x45, 0x63, 0x68, + 0x6f, 0x1a, 0x22, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, + 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, + 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x5b, 0x0a, 0x06, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, + 0x27, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, + 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x55, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x28, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, + 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, + 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x42, 0x25, 0x5a, 0x23, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, + 0x73, 0x69, 0x74, 0x65, 0x2f, 0x78, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x78, 0x65, 0x64, 0x6e, 0x2f, + 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_uplodr_proto_rawDescOnce sync.Once + file_uplodr_proto_rawDescData = file_uplodr_proto_rawDesc +) + +func file_uplodr_proto_rawDescGZIP() []byte { + file_uplodr_proto_rawDescOnce.Do(func() { + file_uplodr_proto_rawDescData = protoimpl.X.CompressGZIP(file_uplodr_proto_rawDescData) + }) + return file_uplodr_proto_rawDescData +} + +var file_uplodr_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_uplodr_proto_goTypes = []interface{}{ + (*UploadReq)(nil), // 0: within.website.x.xedn.uplodr.UploadReq + (*UploadResp)(nil), // 1: within.website.x.xedn.uplodr.UploadResp + (*Variant)(nil), // 2: within.website.x.xedn.uplodr.Variant + (*Echo)(nil), // 3: within.website.x.xedn.uplodr.Echo +} +var file_uplodr_proto_depIdxs = []int32{ + 2, // 0: within.website.x.xedn.uplodr.UploadResp.variants:type_name -> within.website.x.xedn.uplodr.Variant + 3, // 1: within.website.x.xedn.uplodr.Image.Ping:input_type -> within.website.x.xedn.uplodr.Echo + 0, // 2: within.website.x.xedn.uplodr.Image.Upload:input_type -> within.website.x.xedn.uplodr.UploadReq + 3, // 3: within.website.x.xedn.uplodr.Image.Ping:output_type -> within.website.x.xedn.uplodr.Echo + 1, // 4: within.website.x.xedn.uplodr.Image.Upload:output_type -> within.website.x.xedn.uplodr.UploadResp + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_uplodr_proto_init() } +func file_uplodr_proto_init() { + if File_uplodr_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_uplodr_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UploadReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_uplodr_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UploadResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_uplodr_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Variant); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_uplodr_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Echo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_uplodr_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_uplodr_proto_goTypes, + DependencyIndexes: file_uplodr_proto_depIdxs, + MessageInfos: file_uplodr_proto_msgTypes, + }.Build() + File_uplodr_proto = out.File + file_uplodr_proto_rawDesc = nil + file_uplodr_proto_goTypes = nil + file_uplodr_proto_depIdxs = nil +} diff --git a/cmd/xedn/uplodr/pb/uplodr_grpc.pb.go b/cmd/xedn/uplodr/pb/uplodr_grpc.pb.go new file mode 100644 index 0000000..01bc318 --- /dev/null +++ b/cmd/xedn/uplodr/pb/uplodr_grpc.pb.go @@ -0,0 +1,146 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.23.4 +// source: uplodr.proto + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Image_Ping_FullMethodName = "/within.website.x.xedn.uplodr.Image/Ping" + Image_Upload_FullMethodName = "/within.website.x.xedn.uplodr.Image/Upload" +) + +// ImageClient is the client API for Image service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ImageClient interface { + Ping(ctx context.Context, in *Echo, opts ...grpc.CallOption) (*Echo, error) + Upload(ctx context.Context, in *UploadReq, opts ...grpc.CallOption) (*UploadResp, error) +} + +type imageClient struct { + cc grpc.ClientConnInterface +} + +func NewImageClient(cc grpc.ClientConnInterface) ImageClient { + return &imageClient{cc} +} + +func (c *imageClient) Ping(ctx context.Context, in *Echo, opts ...grpc.CallOption) (*Echo, error) { + out := new(Echo) + err := c.cc.Invoke(ctx, Image_Ping_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *imageClient) Upload(ctx context.Context, in *UploadReq, opts ...grpc.CallOption) (*UploadResp, error) { + out := new(UploadResp) + err := c.cc.Invoke(ctx, Image_Upload_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ImageServer is the server API for Image service. +// All implementations must embed UnimplementedImageServer +// for forward compatibility +type ImageServer interface { + Ping(context.Context, *Echo) (*Echo, error) + Upload(context.Context, *UploadReq) (*UploadResp, error) + mustEmbedUnimplementedImageServer() +} + +// UnimplementedImageServer must be embedded to have forward compatible implementations. +type UnimplementedImageServer struct { +} + +func (UnimplementedImageServer) Ping(context.Context, *Echo) (*Echo, error) { + return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedImageServer) Upload(context.Context, *UploadReq) (*UploadResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Upload not implemented") +} +func (UnimplementedImageServer) mustEmbedUnimplementedImageServer() {} + +// UnsafeImageServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ImageServer will +// result in compilation errors. +type UnsafeImageServer interface { + mustEmbedUnimplementedImageServer() +} + +func RegisterImageServer(s grpc.ServiceRegistrar, srv ImageServer) { + s.RegisterService(&Image_ServiceDesc, srv) +} + +func _Image_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Echo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ImageServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Image_Ping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ImageServer).Ping(ctx, req.(*Echo)) + } + return interceptor(ctx, in, info, handler) +} + +func _Image_Upload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UploadReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ImageServer).Upload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Image_Upload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ImageServer).Upload(ctx, req.(*UploadReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Image_ServiceDesc is the grpc.ServiceDesc for Image service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Image_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "within.website.x.xedn.uplodr.Image", + HandlerType: (*ImageServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _Image_Ping_Handler, + }, + { + MethodName: "Upload", + Handler: _Image_Upload_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "uplodr.proto", +} diff --git a/cmd/xedn/uplodr/uplodr.proto b/cmd/xedn/uplodr/uplodr.proto new file mode 100644 index 0000000..f9d4859 --- /dev/null +++ b/cmd/xedn/uplodr/uplodr.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package within.website.x.xedn.uplodr; +option go_package = "within.website/x/cmd/xedn/uplodr/pb"; + +service Image { + rpc Ping(Echo) returns (Echo); + rpc Upload(UploadReq) returns (UploadResp); +} + +message UploadReq { + string file_name = 1; + bytes data = 2; + string folder = 3; +} + +message UploadResp { + repeated Variant variants = 1; +} + +message Variant { + string url = 1; + string mime_type = 2; +} + +message Echo { + string nonce = 1; +} diff --git a/internal/tigris.go b/internal/tigris.go new file mode 100644 index 0000000..91a90e3 --- /dev/null +++ b/internal/tigris.go @@ -0,0 +1,21 @@ +package internal + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +func TigrisClient(ctx context.Context) (*s3.Client, error) { + cfg, err := awsConfig.LoadDefaultConfig(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load Tigris config: %w", err) + } + + return s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev") + }), nil +} |
