diff --git a/api/api_impl/impl.go b/api/api_impl/impl.go new file mode 100644 index 00000000..1f759ae4 --- /dev/null +++ b/api/api_impl/impl.go @@ -0,0 +1,16 @@ +package apiimpl + +import ( + "github.com/jiaozifs/jiaozifs/api" + "github.com/jiaozifs/jiaozifs/controller" + "go.uber.org/fx" +) + +var _ api.ServerInterface = (*APIController)(nil) + +type APIController struct { + fx.In + + controller.VersionController + controller.ObjectController +} diff --git a/api/custom_response.go b/api/custom_response.go index 0823c893..9211856e 100644 --- a/api/custom_response.go +++ b/api/custom_response.go @@ -10,14 +10,13 @@ type JiaozifsResponse struct { } func (response *JiaozifsResponse) RespJSON(v interface{}) { - data, err := json.Marshal(v) + response.Header().Set("Content-Type", "application/json") + response.WriteHeader(http.StatusOK) + err := json.NewEncoder(response).Encode(v) if err != nil { response.RespError(err) return } - response.Header().Set("Content-Type", "application/json") - response.WriteHeader(http.StatusOK) - _, _ = response.Write(data) } func (response *JiaozifsResponse) RespError(err error) { diff --git a/api/jiaozifs.gen.go b/api/jiaozifs.gen.go index 89dbb807..eb5e6000 100644 --- a/api/jiaozifs.gen.go +++ b/api/jiaozifs.gen.go @@ -18,12 +18,55 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" ) const ( - Jwt_tokenScopes = "jwt_token.Scopes" + Basic_authScopes = "basic_auth.Scopes" + Jwt_tokenScopes = "jwt_token.Scopes" ) +// Defines values for ObjectStatsPathType. +const ( + CommonPrefix ObjectStatsPathType = "common_prefix" + Object ObjectStatsPathType = "object" +) + +// ObjectStats defines model for ObjectStats. +type ObjectStats struct { + Checksum string `json:"checksum"` + + // ContentType Object media type + ContentType *string `json:"content_type,omitempty"` + Metadata *ObjectUserMetadata `json:"metadata,omitempty"` + + // Mtime Unix Epoch in seconds + Mtime int64 `json:"mtime"` + Path string `json:"path"` + PathType ObjectStatsPathType `json:"path_type"` + + // PhysicalAddress The location of the object on the underlying object store. + // Formatted as a native URI with the object store type as scheme ("s3://...", "gs://...", etc.) + // Or, in the case of presign=true, will be an HTTP URL to be consumed via regular HTTP GET + PhysicalAddress string `json:"physical_address"` + + // PhysicalAddressExpiry If present and nonzero, physical_address is a pre-signed URL and + // will expire at this Unix Epoch time. This will be shorter than + // the pre-signed URL lifetime if an authentication token is about + // to expire. + // + // This field is *optional*. + PhysicalAddressExpiry *int64 `json:"physical_address_expiry,omitempty"` + SizeBytes *int64 `json:"size_bytes,omitempty"` +} + +// ObjectStatsPathType defines model for ObjectStats.PathType. +type ObjectStatsPathType string + +// ObjectUserMetadata defines model for ObjectUserMetadata. +type ObjectUserMetadata map[string]string + // VersionResult defines model for VersionResult. type VersionResult struct { // ApiVersion runtime version @@ -33,6 +76,54 @@ type VersionResult struct { Version string `json:"version"` } +// DeleteObjectParams defines parameters for DeleteObject. +type DeleteObjectParams struct { + // Path relative to the ref + Path string `form:"path" json:"path"` +} + +// GetObjectParams defines parameters for GetObject. +type GetObjectParams struct { + Presign *bool `form:"presign,omitempty" json:"presign,omitempty"` + + // Path relative to the ref + Path string `form:"path" json:"path"` + + // Range Byte range to retrieve + Range *string `json:"Range,omitempty"` +} + +// HeadObjectParams defines parameters for HeadObject. +type HeadObjectParams struct { + // Path relative to the ref + Path string `form:"path" json:"path"` + + // Range Byte range to retrieve + Range *string `json:"Range,omitempty"` +} + +// UploadObjectMultipartBody defines parameters for UploadObject. +type UploadObjectMultipartBody struct { + // Content Only a single file per upload which must be named "content". + Content *openapi_types.File `json:"content,omitempty"` +} + +// UploadObjectParams defines parameters for UploadObject. +type UploadObjectParams struct { + // StorageClass Deprecated, this capability will not be supported in future releases. + StorageClass *string `form:"storageClass,omitempty" json:"storageClass,omitempty"` + + // Path relative to the ref + Path string `form:"path" json:"path"` + + // IfNoneMatch Currently supports only "*" to allow uploading an object only if one doesn't exist yet. + // Deprecated, this capability will not be supported in future releases. + IfNoneMatch *string `json:"If-None-Match,omitempty"` +} + +// UploadObjectMultipartRequestBody defines body for UploadObject for multipart/form-data ContentType. +type UploadObjectMultipartRequestBody UploadObjectMultipartBody + // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -106,10 +197,70 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // DeleteObject request + DeleteObject(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetObject request + GetObject(ctx context.Context, repository string, params *GetObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // HeadObject request + HeadObject(ctx context.Context, repository string, params *HeadObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UploadObjectWithBody request with any body + UploadObjectWithBody(ctx context.Context, repository string, params *UploadObjectParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetVersion request GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) DeleteObject(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteObjectRequest(c.Server, repository, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetObject(ctx context.Context, repository string, params *GetObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetObjectRequest(c.Server, repository, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) HeadObject(ctx context.Context, repository string, params *HeadObjectParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewHeadObjectRequest(c.Server, repository, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UploadObjectWithBody(ctx context.Context, repository string, params *UploadObjectParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUploadObjectRequestWithBody(c.Server, repository, params, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetVersionRequest(c.Server) if err != nil { @@ -122,6 +273,293 @@ func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) return c.Client.Do(req) } +// NewDeleteObjectRequest generates requests for DeleteObject +func NewDeleteObjectRequest(server string, repository string, params *DeleteObjectParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "repository", runtime.ParamLocationPath, repository) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/repositories/%s/objects", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetObjectRequest generates requests for GetObject +func NewGetObjectRequest(server string, repository string, params *GetObjectParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "repository", runtime.ParamLocationPath, repository) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/repositories/%s/objects", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Presign != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "presign", runtime.ParamLocationQuery, *params.Presign); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + if params.Range != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "Range", runtime.ParamLocationHeader, *params.Range) + if err != nil { + return nil, err + } + + req.Header.Set("Range", headerParam0) + } + + } + + return req, nil +} + +// NewHeadObjectRequest generates requests for HeadObject +func NewHeadObjectRequest(server string, repository string, params *HeadObjectParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "repository", runtime.ParamLocationPath, repository) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/repositories/%s/objects", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("HEAD", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + if params.Range != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "Range", runtime.ParamLocationHeader, *params.Range) + if err != nil { + return nil, err + } + + req.Header.Set("Range", headerParam0) + } + + } + + return req, nil +} + +// NewUploadObjectRequestWithBody generates requests for UploadObject with any type of body +func NewUploadObjectRequestWithBody(server string, repository string, params *UploadObjectParams, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "repository", runtime.ParamLocationPath, repository) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/repositories/%s/objects", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.StorageClass != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "storageClass", runtime.ParamLocationQuery, *params.StorageClass); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + if params != nil { + + if params.IfNoneMatch != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "If-None-Match", runtime.ParamLocationHeader, *params.IfNoneMatch) + if err != nil { + return nil, err + } + + req.Header.Set("If-None-Match", headerParam0) + } + + } + + return req, nil +} + // NewGetVersionRequest generates requests for GetVersion func NewGetVersionRequest(server string) (*http.Request, error) { var err error @@ -160,71 +598,278 @@ func (c *Client) applyEditors(ctx context.Context, req *http.Request, additional return err } } - return nil -} + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // DeleteObjectWithResponse request + DeleteObjectWithResponse(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*DeleteObjectResponse, error) + + // GetObjectWithResponse request + GetObjectWithResponse(ctx context.Context, repository string, params *GetObjectParams, reqEditors ...RequestEditorFn) (*GetObjectResponse, error) + + // HeadObjectWithResponse request + HeadObjectWithResponse(ctx context.Context, repository string, params *HeadObjectParams, reqEditors ...RequestEditorFn) (*HeadObjectResponse, error) + + // UploadObjectWithBodyWithResponse request with any body + UploadObjectWithBodyWithResponse(ctx context.Context, repository string, params *UploadObjectParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UploadObjectResponse, error) + + // GetVersionWithResponse request + GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) +} + +type DeleteObjectResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteObjectResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteObjectResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetObjectResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GetObjectResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetObjectResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type HeadObjectResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r HeadObjectResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r HeadObjectResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type UploadObjectResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *ObjectStats +} + +// Status returns HTTPResponse.Status +func (r UploadObjectResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UploadObjectResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetVersionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VersionResult +} + +// Status returns HTTPResponse.Status +func (r GetVersionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetVersionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// DeleteObjectWithResponse request returning *DeleteObjectResponse +func (c *ClientWithResponses) DeleteObjectWithResponse(ctx context.Context, repository string, params *DeleteObjectParams, reqEditors ...RequestEditorFn) (*DeleteObjectResponse, error) { + rsp, err := c.DeleteObject(ctx, repository, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteObjectResponse(rsp) +} + +// GetObjectWithResponse request returning *GetObjectResponse +func (c *ClientWithResponses) GetObjectWithResponse(ctx context.Context, repository string, params *GetObjectParams, reqEditors ...RequestEditorFn) (*GetObjectResponse, error) { + rsp, err := c.GetObject(ctx, repository, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetObjectResponse(rsp) +} + +// HeadObjectWithResponse request returning *HeadObjectResponse +func (c *ClientWithResponses) HeadObjectWithResponse(ctx context.Context, repository string, params *HeadObjectParams, reqEditors ...RequestEditorFn) (*HeadObjectResponse, error) { + rsp, err := c.HeadObject(ctx, repository, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseHeadObjectResponse(rsp) +} + +// UploadObjectWithBodyWithResponse request with arbitrary body returning *UploadObjectResponse +func (c *ClientWithResponses) UploadObjectWithBodyWithResponse(ctx context.Context, repository string, params *UploadObjectParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UploadObjectResponse, error) { + rsp, err := c.UploadObjectWithBody(ctx, repository, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUploadObjectResponse(rsp) +} + +// GetVersionWithResponse request returning *GetVersionResponse +func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) { + rsp, err := c.GetVersion(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetVersionResponse(rsp) +} + +// ParseDeleteObjectResponse parses an HTTP response from a DeleteObjectWithResponse call +func ParseDeleteObjectResponse(rsp *http.Response) (*DeleteObjectResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteObjectResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } -// ClientWithResponses builds on ClientInterface to offer response payloads -type ClientWithResponses struct { - ClientInterface + return response, nil } -// NewClientWithResponses creates a new ClientWithResponses, which wraps -// Client with return type handling -func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { - client, err := NewClient(server, opts...) +// ParseGetObjectResponse parses an HTTP response from a GetObjectWithResponse call +func ParseGetObjectResponse(rsp *http.Response) (*GetObjectResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return &ClientWithResponses{client}, nil -} -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() - return nil + response := &GetObjectResponse{ + Body: bodyBytes, + HTTPResponse: rsp, } -} - -// ClientWithResponsesInterface is the interface specification for the client with responses above. -type ClientWithResponsesInterface interface { - // GetVersionWithResponse request - GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) -} -type GetVersionResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *VersionResult + return response, nil } -// Status returns HTTPResponse.Status -func (r GetVersionResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status +// ParseHeadObjectResponse parses an HTTP response from a HeadObjectWithResponse call +func ParseHeadObjectResponse(rsp *http.Response) (*HeadObjectResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err } - return http.StatusText(0) -} -// StatusCode returns HTTPResponse.StatusCode -func (r GetVersionResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode + response := &HeadObjectResponse{ + Body: bodyBytes, + HTTPResponse: rsp, } - return 0 + + return response, nil } -// GetVersionWithResponse request returning *GetVersionResponse -func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) { - rsp, err := c.GetVersion(ctx, reqEditors...) +// ParseUploadObjectResponse parses an HTTP response from a UploadObjectWithResponse call +func ParseUploadObjectResponse(rsp *http.Response) (*UploadObjectResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseGetVersionResponse(rsp) + + response := &UploadObjectResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest ObjectStats + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil } // ParseGetVersionResponse parses an HTTP response from a GetVersionWithResponse call @@ -255,6 +900,18 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { // ServerInterface represents all server handlers. type ServerInterface interface { + // delete object. Missing objects will not return a NotFound error. + // (DELETE /repositories/{repository}/objects) + DeleteObject(w *JiaozifsResponse, r *http.Request, repository string, params DeleteObjectParams) + // get object content + // (GET /repositories/{repository}/objects) + GetObject(w *JiaozifsResponse, r *http.Request, repository string, params GetObjectParams) + // check if object exists + // (HEAD /repositories/{repository}/objects) + HeadObject(w *JiaozifsResponse, r *http.Request, repository string, params HeadObjectParams) + + // (POST /repositories/{repository}/objects) + UploadObject(w *JiaozifsResponse, r *http.Request, repository string, params UploadObjectParams) // return program and runtime version // (GET /version) GetVersion(w *JiaozifsResponse, r *http.Request) @@ -264,6 +921,29 @@ type ServerInterface interface { type Unimplemented struct{} +// delete object. Missing objects will not return a NotFound error. +// (DELETE /repositories/{repository}/objects) +func (_ Unimplemented) DeleteObject(w *JiaozifsResponse, r *http.Request, repository string, params DeleteObjectParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// get object content +// (GET /repositories/{repository}/objects) +func (_ Unimplemented) GetObject(w *JiaozifsResponse, r *http.Request, repository string, params GetObjectParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// check if object exists +// (HEAD /repositories/{repository}/objects) +func (_ Unimplemented) HeadObject(w *JiaozifsResponse, r *http.Request, repository string, params HeadObjectParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (POST /repositories/{repository}/objects) +func (_ Unimplemented) UploadObject(w *JiaozifsResponse, r *http.Request, repository string, params UploadObjectParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // return program and runtime version // (GET /version) func (_ Unimplemented) GetVersion(w *JiaozifsResponse, r *http.Request) { @@ -279,6 +959,277 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// DeleteObject operation middleware +func (siw *ServerInterfaceWrapper) DeleteObject(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "repository" ------------- + var repository string + + err = runtime.BindStyledParameterWithOptions("simple", "repository", chi.URLParam(r, "repository"), &repository, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "repository", Err: err}) + return + } + + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + + ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + + // Parameter object where we will unmarshal all parameters from the context + var params DeleteObjectParams + + // ------------- Required query parameter "path" ------------- + + if paramValue := r.URL.Query().Get("path"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteObject(&JiaozifsResponse{w}, r, repository, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetObject operation middleware +func (siw *ServerInterfaceWrapper) GetObject(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "repository" ------------- + var repository string + + err = runtime.BindStyledParameterWithOptions("simple", "repository", chi.URLParam(r, "repository"), &repository, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "repository", Err: err}) + return + } + + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + + ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetObjectParams + + // ------------- Optional query parameter "presign" ------------- + + err = runtime.BindQueryParameter("form", true, false, "presign", r.URL.Query(), ¶ms.Presign) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "presign", Err: err}) + return + } + + // ------------- Required query parameter "path" ------------- + + if paramValue := r.URL.Query().Get("path"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + return + } + + headers := r.Header + + // ------------- Optional header parameter "Range" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Range")]; found { + var Range string + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Range", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Range", valueList[0], &Range, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Range", Err: err}) + return + } + + params.Range = &Range + + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetObject(&JiaozifsResponse{w}, r, repository, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// HeadObject operation middleware +func (siw *ServerInterfaceWrapper) HeadObject(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "repository" ------------- + var repository string + + err = runtime.BindStyledParameterWithOptions("simple", "repository", chi.URLParam(r, "repository"), &repository, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "repository", Err: err}) + return + } + + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + + ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + + // Parameter object where we will unmarshal all parameters from the context + var params HeadObjectParams + + // ------------- Required query parameter "path" ------------- + + if paramValue := r.URL.Query().Get("path"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + return + } + + headers := r.Header + + // ------------- Optional header parameter "Range" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Range")]; found { + var Range string + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Range", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Range", valueList[0], &Range, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Range", Err: err}) + return + } + + params.Range = &Range + + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.HeadObject(&JiaozifsResponse{w}, r, repository, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// UploadObject operation middleware +func (siw *ServerInterfaceWrapper) UploadObject(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "repository" ------------- + var repository string + + err = runtime.BindStyledParameterWithOptions("simple", "repository", chi.URLParam(r, "repository"), &repository, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "repository", Err: err}) + return + } + + ctx = context.WithValue(ctx, Jwt_tokenScopes, []string{"aaaa"}) + + ctx = context.WithValue(ctx, Basic_authScopes, []string{"aaaaa"}) + + // Parameter object where we will unmarshal all parameters from the context + var params UploadObjectParams + + // ------------- Optional query parameter "storageClass" ------------- + + err = runtime.BindQueryParameter("form", true, false, "storageClass", r.URL.Query(), ¶ms.StorageClass) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "storageClass", Err: err}) + return + } + + // ------------- Required query parameter "path" ------------- + + if paramValue := r.URL.Query().Get("path"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + return + } + + headers := r.Header + + // ------------- Optional header parameter "If-None-Match" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("If-None-Match")]; found { + var IfNoneMatch string + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "If-None-Match", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "If-None-Match", valueList[0], &IfNoneMatch, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "If-None-Match", Err: err}) + return + } + + params.IfNoneMatch = &IfNoneMatch + + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UploadObject(&JiaozifsResponse{w}, r, repository, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // GetVersion operation middleware func (siw *ServerInterfaceWrapper) GetVersion(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -409,6 +1360,18 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/repositories/{repository}/objects", wrapper.DeleteObject) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/repositories/{repository}/objects", wrapper.GetObject) + }) + r.Group(func(r chi.Router) { + r.Head(options.BaseURL+"/repositories/{repository}/objects", wrapper.HeadObject) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/repositories/{repository}/objects", wrapper.UploadObject) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/version", wrapper.GetVersion) }) @@ -419,15 +1382,37 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/3xTwW7bMAz9FYPb0bPd7uZbMXRbt2Eo1qI7BEHAyEzM1JY0iW6QBf73QXLqOOiSUyzq", - "PfJReW8PyrTWaNLiodyDVzW1GD+fyHk2+hf5rpFQsM5YcsIUr9Hy4mWAhGNFXjm2Eo/gOi3cUvIKSEF2", - "lqAEL471GvoUznKtM2uH7Xlun4KjPx07qqCcwRE3lTQfaWa5ISWR5kl1jmX3ELYc1liiZ7XATupx/UCK", - "5ePoWsQG0cqYZ6YRzkHvUIMUNEYqayGnsYmohSd/ugVa/k670GyzlYWYZ4pvsCR05D4b16JACd9+P0I6", - "kRNv3+oxXKnLakbEJSUe2+ZymxFxvk14YNYr8/Yf3TCav7zyydfHx/vk5v4OUmhYkfYUwIcRNxZVTcl1", - "VkAKnWsOa/oyz7fbbYbxOjNunR+4Pv9x9+n258Pth+usyGppm7CLsDQ0HTrMG+0GV1mRFfHxLGm0DCV8", - "jKUULEodXZFP3Lmm6P7gfQwL3VVQwheSp9F3jrw1QVDAXRdF+FFGC2kZkmIbVpGbb/zQdMhZ+HrvaAUl", - "vMuPQcwPKcxPIxhf+HJWpiaHcrafemw27+cp+K5t0e1CRkk6p5PXFqir5D+xxbUPIVOmbUOq+osTABER", - "5n16mquhHi6CAHKhfeSesckASUhX1rCW0Q45Ws5frqCf9/8CAAD//zGNbEq4BAAA", + "H4sIAAAAAAAC/+RYX2/cuBH/KgR7QBNXq13bQR4MHA53OfvOrZMLHDt9OLnGrDRaMaFIHjmyvTH2uxck", + "pf0n7cYJmgLXvhhecTgz/HHmN8N55LmujVaoyPGTR+7yCmsI//42/YA5vSOIK8Zqg5YEhl95hflH19T+", + "f5ob5CfckRVqxhcJz7UiVHQbFx55gS63wpDQip+0elmNhQAWRJK+ihoJCiDw27+zWPIT/pfxytVx6+c4", + "Krt2aF93O/xuEvWA5WslHtip0XnFhGIOc60KxxNealsD8RMuFL18sXJHKMIZWq/RAFWDZ/ULy4Oi8oj8", + "7jGttbo1FkvxwBOug5f8ZuCgppo7kYO8haKw6Fzf66sKmdQ5+J9Ml4wqZFEh0yr8alSBVs6FmnULjrTF", + "NFNn4WSEBQPHgCkgcYfs+vKc3Quq1lWFHeE6vGiAF9mzjLvjk/E4TdOMJyzjM7f6hZSnzzP1m008ml5V", + "Dg69h8aiEzP1PdkGE3YvpGRTZKDYr1dXb9n15QUj7b/kWrmmxoLdCWAWZ40EG2V+Ob3KFH8CXLf4YISd", + "91E7j26gIgaqYEqrT2h1wrYVMOGBMRZH3mUsgnugikwFv4N6ZECMKuHYWgT5EEsZu/KfuyO6SltCy6gC", + "lSkPyZZiKUr0G5koPR7QUIWKRHu5pD+iCg5NdUOZIt3aTzOVqWCpFCgLL3Kgw0lBHqQBqSfEsBOf8HY6", + "p5jBn92wSLjFPxphsQgx3WX8QMy2+bGeDV0SrmK+TYJFwgeS9uSRQ1GIeKS3G1TTi4GevvdondDqEl0j", + "qc9VYMTtXRTpx4ltVLiQTmAg5nbuNVbPLNS7925BuJJbd6mPkL8rzBsraP4uJGI4xhScyG99yCyJ2m8K", + "n1emKyITOVh/FLgUF97f+I0nXEHdXbVV/h4bqm4dus1TgBH/wLlX9uGebkNwBj8QLNqzLnz+/s8rnqy5", + "E1b7/mhR5Pu9WUrs88RBLferWUrsVuMBFqrU/Rv9IEB/EqWLLPTj23OecClyVC7we2viRwN5hewonfCE", + "N1a2x/TceH9/n0JYTrWdjdu9bnxx/ur0zbvT0VE6SSuqZYhjQRLXjUZ7y3Djh+kknQTwDCowgp/w4/Ap", + "JlqIirFFo50gbQW68ePy13wxjuHUVhSJFI7gEyOwzXnBT/jP4XvMR+6D1Rnt/fWSR5MXfYDaahH1Fcw1", + "eY7OlY2U4XpeTA6Hyq6/Dm3FJyyi0HFf6EzbqSgKVFFiwPQbTWe6UVHF0aQvQFqzGtSc+ZxDRy5mUlPX", + "4CtEC0Jb8FL2Wji3KpktiStNzCI1VjFgnUWG1mqb+kCCmfOZ3EF7s0j4DKkP7C9IS1QNWKiR0Pqt207/", + "NCdkFtQMfVG0SFbgnQ9nfIDahPgIlP39ZHQ4OTrmSYz6CqEIadaG5KXX0OVhoFPj6771sv+KCp49y7Li", + "YOT/JD+wH57/7fl3Q4zVptUfDdr5Sn9b0jcstFunWksExReLm14EhVtq28FIxUa21W6sc0IaObII9ar1", + "3ChNU6EgeLHt5SIZjsvOVNICFNx4FT+OLlDN1sgTnloFT69gtrmrT/MX4Gj0WheiFJ7r9wl78aPJy/8W", + "MgYsCZDsWyLU7Y9RuLH9a8PwW6B+PDnqs8YlFsJ6ZEj3G8FS27UmeRO0i7Yn/7zdJ7Librr1rFQuue9w", + "slMwtout2MuhwwZmxIKFq/IMx94BCVcKmEr8amqdIfUDbIgsPX59tvwVofhz0uUOyttxOcIj9yfhpqew", + "CAtd1P8jlfxPprQvHSW0b6hN6e6pwBzaO7SxI9oigfBC9C/b7XgfIoKtLBcxyMIjss3RVSvL159RYaiw", + "7yp7LzyUcexBOnC5xbKjhe0mJ9p/uq2bhBvtBtq/ayP1PkozFnOglYlNj39eridx7JCDgamQguarLnWK", + "zDXGaOuvXihWNtRYfzqJ4NClO87oSFuY4SsJ4d3+GRz3+/mqsRYVyXnniWNayTnL+EHGQz2VUt+zJoDh", + "W21Qq8mVnIdQUcgKjU79tY0XNkdKM/UfgSAMRlaF4WBXNTgvR2+0wtFroLzaVRWy7GBnAQgJ9JMu5t+o", + "qUt43UgSnoTHXnrUzUzWPN0cz6582Bq+etyB+YePRFYKicygba+I3Vcir1jduICtR6dgWacs4+n6nGmP", + "s9vjjDgH2aiSh3uQ+uC2m6rPT4DjmHr3w6Bemw6/GKrR70GKItg/jdT2BI5ng5u+6nHbWLldEgZ61bc2", + "zKzDmOwMhMQvfQz3mDjhD6O75SlG+JDLpsDRNMSyz3m/a7w2Adv12n2/nG19wRPwy256c8w39NTZmset", + "D9IC8a7NsX6/8am7Kl7to79TAapgA6PBFr443+c3i70WOACAL3abs7v43S94B0I5Hep0l1OhruKqwmgR", + "Gus4chqDEeO7Q764Wfw7AAD//58N5ajGGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/swagger.yml b/api/swagger.yml index a9acb731..1ab95d18 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -49,6 +49,91 @@ components: api_version: type: string description: runtime version + ObjectUserMetadata: + type: object + additionalProperties: + type: string + + ObjectStats: + type: object + required: + - checksum + - physical_address + - path + - path_type + - mtime + properties: + path: + type: string + path_type: + type: string + enum: [common_prefix, object] + physical_address: + type: string + description: | + The location of the object on the underlying object store. + Formatted as a native URI with the object store type as scheme ("s3://...", "gs://...", etc.) + Or, in the case of presign=true, will be an HTTP URL to be consumed via regular HTTP GET + physical_address_expiry: + type: integer + format: int64 + description: | + If present and nonzero, physical_address is a pre-signed URL and + will expire at this Unix Epoch time. This will be shorter than + the pre-signed URL lifetime if an authentication token is about + to expire. + + This field is *optional*. + checksum: + type: string + size_bytes: + type: integer + format: int64 + mtime: + type: integer + format: int64 + description: Unix Epoch in seconds + metadata: + $ref: "#/components/schemas/ObjectUserMetadata" + content_type: + type: string + description: Object media type + + ObjectStatsList: + type: object + required: + - pagination + - results + properties: + pagination: + $ref: "#/components/schemas/Pagination" + results: + type: array + items: + $ref: "#/components/schemas/ObjectStats" + + Pagination: + type: object + required: + - has_more + - max_per_page + - results + - next_offset + properties: + has_more: + type: boolean + description: Next page is available + next_offset: + type: string + description: Token used to retrieve the next page + results: + type: integer + minimum: 0 + description: Number of values found in the results + max_per_page: + type: integer + minimum: 0 + description: Maximal number of entries per page Error: type: object required: @@ -74,3 +159,223 @@ paths: schema: $ref: "#/components/schemas/VersionResult" + /repositories/{repository}/objects: + parameters: + - in: path + name: repository + required: true + schema: + type: string + - in: query + name: path + description: relative to the ref + required: true + schema: + type: string + get: + tags: + - objects + operationId: getObject + summary: get object content + parameters: + - in: header + name: Range + description: Byte range to retrieve + example: "bytes=0-1023" + required: false + schema: + type: string + pattern: '^bytes=((\d*-\d*,? ?)+)$' + - in: query + name: presign + required: false + schema: + type: boolean + responses: + 200: + description: object content + content: + application/octet-stream: + schema: + type: string + format: binary + headers: + Content-Length: + schema: + type: integer + format: int64 + Last-Modified: + schema: + type: string + ETag: + schema: + type: string + 206: + description: partial object content + content: + application/octet-stream: + schema: + type: string + format: binary + headers: + Content-Length: + schema: + type: integer + format: int64 + Content-Range: + schema: + type: string + pattern: '^bytes=((\d*-\d*,? ?)+)$' + Last-Modified: + schema: + type: string + ETag: + schema: + type: string + 302: + description: Redirect to a pre-signed URL for the object + headers: + Location: + schema: + type: string + 401: + description: Unauthorized + 404: + description: object not found + 410: + description: object expired + 416: + description: Requested Range Not Satisfiable + 420: + description: too many requests + head: + tags: + - objects + operationId: headObject + summary: check if object exists + parameters: + - in: header + name: Range + description: Byte range to retrieve + example: "bytes=0-1023" + required: false + schema: + type: string + pattern: '^bytes=((\d*-\d*,? ?)+)$' + responses: + 200: + description: object exists + headers: + Content-Length: + schema: + type: integer + format: int64 + Last-Modified: + schema: + type: string + ETag: + schema: + type: string + 206: + description: partial object content info + headers: + Content-Length: + schema: + type: integer + format: int64 + Content-Range: + schema: + type: string + pattern: '^bytes=((\d*-\d*,? ?)+)$' + Last-Modified: + schema: + type: string + ETag: + schema: + type: string + 401: + description: Unauthorized + 404: + description: object not found + 410: + description: object expired + 416: + description: Requested Range Not Satisfiable + 420: + description: too many requests + default: + description: internal server error + post: + tags: + - objects + operationId: uploadObject + x-validation-exclude-body: true + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + content: + description: Only a single file per upload which must be named "content". + type: string + format: binary + application/octet-stream: + schema: + type: string + format: binary + + parameters: + - in: query + name: storageClass + description: Deprecated, this capability will not be supported in future releases. + required: false + deprecated: true + schema: + type: string + - in: header + name: If-None-Match + description: | + Currently supports only "*" to allow uploading an object only if one doesn't exist yet. + Deprecated, this capability will not be supported in future releases. + example: "*" + required: false + deprecated: true + schema: + type: string + pattern: '^\*$' # Currently, only "*" is supported + responses: + 201: + description: object metadata + content: + application/json: + schema: + $ref: "#/components/schemas/ObjectStats" + 400: + description: ValidationError + 401: + description: Unauthorized ValidationError + 403: + description: Forbidden + 404: + description: url not found + 412: + description: PreconditionFailed + 420: + description: too many requests + delete: + tags: + - objects + operationId: deleteObject + summary: delete object. Missing objects will not return a NotFound error. + responses: + 204: + description: object deleted successfully + 401: + description: Unauthorized + 403: + description: Forbidden + 404: + description: NotFound + 420: + description: too many requests diff --git a/cmd/daemon.go b/cmd/daemon.go index fbb6d5cb..cd5e8038 100644 --- a/cmd/daemon.go +++ b/cmd/daemon.go @@ -53,6 +53,10 @@ var daemonCmd = &cobra.Command{ fx_opt.Override(new(*bun.DB), models.SetupDatabase), fx_opt.Override(fx_opt.NextInvoke(), migrations.MigrateDatabase), fx_opt.Override(new(*models.IUserRepo), models.NewUserRepo), + fx_opt.Override(new(*models.IRepositoryRepo), models.NewRepositoryRepo), + fx_opt.Override(new(*models.IRefRepo), models.NewRefRepo), + fx_opt.Override(new(*models.IObjectRepo), models.NewObjectRepo), + //api fx_opt.Override(fx_opt.NextInvoke(), apiImpl.SetupAPI), ) diff --git a/controller/object_ctl.go b/controller/object_ctl.go new file mode 100644 index 00000000..84cbd926 --- /dev/null +++ b/controller/object_ctl.go @@ -0,0 +1,32 @@ +package controller + +import ( + "net/http" + + "github.com/jiaozifs/jiaozifs/api" + "go.uber.org/fx" +) + +type ObjectController struct { + fx.In +} + +func (A ObjectController) DeleteObject(_ *api.JiaozifsResponse, r *http.Request, repository string, params api.DeleteObjectParams) { //nolint + //TODO implement me + panic("implement me") +} + +func (A ObjectController) GetObject(_ *api.JiaozifsResponse, r *http.Request, repository string, params api.GetObjectParams) { //nolint + //TODO implement me + panic("implement me") +} + +func (A ObjectController) HeadObject(_ *api.JiaozifsResponse, r *http.Request, repository string, params api.HeadObjectParams) { //nolint + //TODO implement me + panic("implement me") +} + +func (A ObjectController) UploadObject(_ *api.JiaozifsResponse, r *http.Request, repository string, params api.UploadObjectParams) { //nolint + //TODO implement me + panic("implement me") +} diff --git a/api/api_impl/common.go b/controller/version_ctl.go similarity index 65% rename from api/api_impl/common.go rename to controller/version_ctl.go index dd71cde3..c1cfbb30 100644 --- a/api/api_impl/common.go +++ b/controller/version_ctl.go @@ -1,4 +1,4 @@ -package apiimpl +package controller import ( "net/http" @@ -8,13 +8,11 @@ import ( "go.uber.org/fx" ) -var _ api.ServerInterface = (*APIController)(nil) - -type APIController struct { +type VersionController struct { fx.In } -func (A APIController) GetVersion(w *api.JiaozifsResponse, _ *http.Request) { +func (A VersionController) GetVersion(w *api.JiaozifsResponse, _ *http.Request) { swagger, err := api.GetSwagger() if err != nil { w.RespError(err) diff --git a/go.mod b/go.mod index 519d3f4b..9147bc05 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.47.1 github.com/aws/smithy-go v1.18.1 github.com/benburkert/dns v0.0.0-20190225204957-d356cf78cdfc + github.com/brianvoe/gofakeit/v6 v6.25.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deepmap/oapi-codegen/v2 v2.0.1-0.20231120160225-add3126ee845 github.com/fergusstrange/embedded-postgres v1.25.0 @@ -30,8 +31,10 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 github.com/oapi-codegen/nethttp-middleware v1.0.1 + github.com/oapi-codegen/runtime v1.1.0 github.com/ory/dockertest/v3 v3.10.0 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 + github.com/pjbgf/sha1cd v0.3.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 github.com/puzpuzpuz/xsync v1.5.2 @@ -47,6 +50,7 @@ require ( golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/oauth2 v0.13.0 google.golang.org/api v0.147.0 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 ) @@ -60,6 +64,7 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.3 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.8 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.7 // indirect @@ -106,6 +111,8 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -129,6 +136,7 @@ require ( github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index 27da620d..6e6f6997 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,9 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aws/aws-sdk-go-v2 v1.23.4 h1:2P20ZjH0ouSAu/6yZep8oCmTReathLuEu6dwoqEgjts= github.com/aws/aws-sdk-go-v2 v1.23.4/go.mod h1:t3szzKfP0NeRU27uBFczDivYJjsmSnqI8kIvKyWb9ds= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.3 h1:Zx9+31KyB8wQna6SXFWOewlgoY5uGdDAu6PTOEU3OQI= @@ -109,6 +112,9 @@ github.com/benburkert/dns v0.0.0-20190225204957-d356cf78cdfc h1:eyDlmf21vuKN61Wo github.com/benburkert/dns v0.0.0-20190225204957-d356cf78cdfc/go.mod h1:6ul4nJKqsreAIBK5lUkibcUn2YBU6CvDzlKDH+dtZsQ= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/brianvoe/gofakeit/v6 v6.25.0 h1:ZpFjktOpLZUeF8q223o0rUuXtA+m5qW5srjvVi+JkXk= +github.com/brianvoe/gofakeit/v6 v6.25.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -288,6 +294,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= @@ -299,6 +306,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -345,6 +353,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/oapi-codegen/nethttp-middleware v1.0.1 h1:ZWvwfnMU0eloHX1VEJmQscQm3741t0vCm0eSIie1NIo= github.com/oapi-codegen/nethttp-middleware v1.0.1/go.mod h1:P7xtAvpoqNB+5obR9qRCeefH7YlXWSK3KgPs/9WB8tE= +github.com/oapi-codegen/runtime v1.1.0 h1:rJpoNUawn5XTvekgfkvSZr0RqEnoYpFkyvrzfWeFKWM= +github.com/oapi-codegen/runtime v1.1.0/go.mod h1:BeSfBkWWWnAnGdyS+S/GnlbmHKzf8/hwkvelJZDeKA8= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= @@ -359,8 +369,11 @@ github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55F github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -380,7 +393,9 @@ github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/ github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY= github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -402,6 +417,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/models/filemode/filemode.go b/models/filemode/filemode.go new file mode 100644 index 00000000..9840970d --- /dev/null +++ b/models/filemode/filemode.go @@ -0,0 +1,190 @@ +package filemode + +import ( + "encoding/binary" + "fmt" + "os" + "strconv" +) + +// A FileMode represents the kind of tree entries used by git. It +// resembles regular file systems modes, although FileModes are +// considerably simpler (there are not so many), and there are some, +// like Submodule that has no file system equivalent. +// +// The code is copied from go git just to ensure compatibility and only partially implemented. +type FileMode uint32 + +const ( + // Empty is used as the FileMode of tree elements when comparing + // trees in the following situations: + // + // - the mode of tree elements before their creation. - the mode of + // tree elements after their deletion. - the mode of unmerged + // elements when checking the index. + // + // Empty has no file system equivalent. As Empty is the zero value + // of FileMode, it is also returned by New and + // NewFromOsNewFromOSFileMode along with an error, when they fail. + Empty FileMode = 0 + // Dir represent a Directory. + Dir FileMode = 0040000 + // Regular represent non-executable files. Please note this is not + // the same as golang regular files, which include executable files. + Regular FileMode = 0100644 + // Deprecated represent non-executable files with the group writable + // bit set. This mode was supported by the first versions of git, + // but it has been deprecated nowadays. This library uses them + // internally, so you can read old packfiles, but will treat them as + // Regulars when interfacing with the outside world. This is the + // standard git behaviour. + Deprecated FileMode = 0100664 + // Executable represents executable files. + Executable FileMode = 0100755 + // Symlink represents symbolic links to files. + Symlink FileMode = 0120000 + // Submodule represents git submodules. This mode has no file system + // equivalent. + Submodule FileMode = 0160000 +) + +// New takes the octal string representation of a FileMode and returns +// the FileMode and a nil error. If the string can not be parsed to a +// 32 bit unsigned octal number, it returns Empty and the parsing error. +// +// Example: "40000" means Dir, "100644" means Regular. +// +// Please note this function does not check if the returned FileMode +// is valid in git or if it is malformed. For instance, "1" will +// return the malformed FileMode(1) and a nil error. +func New(s string) (FileMode, error) { + n, err := strconv.ParseUint(s, 8, 32) + if err != nil { + return Empty, err + } + + return FileMode(n), nil +} + +// NewFromOSFileMode returns the FileMode used by git to represent +// the provided file system modes and a nil error on success. If the +// file system mode cannot be mapped to any valid git mode (as with +// sockets or named pipes), it will return Empty and an error. +// +// Note that some git modes cannot be generated from os.FileModes, like +// Deprecated and Submodule; while Empty will be returned, along with an +// error, only when the method fails. +func NewFromOSFileMode(m os.FileMode) (FileMode, error) { + if m.IsRegular() { + if isSetTemporary(m) { + return Empty, fmt.Errorf("no equivalent git mode for %s", m) + } + if isSetCharDevice(m) { + return Empty, fmt.Errorf("no equivalent git mode for %s", m) + } + if isSetUserExecutable(m) { + return Executable, nil + } + return Regular, nil + } + + if m.IsDir() { + return Dir, nil + } + + if isSetSymLink(m) { + return Symlink, nil + } + + return Empty, fmt.Errorf("no equivalent git mode for %s", m) +} + +func isSetCharDevice(m os.FileMode) bool { + return m&os.ModeCharDevice != 0 +} + +func isSetTemporary(m os.FileMode) bool { + return m&os.ModeTemporary != 0 +} + +func isSetUserExecutable(m os.FileMode) bool { + return m&0100 != 0 +} + +func isSetSymLink(m os.FileMode) bool { + return m&os.ModeSymlink != 0 +} + +// Bytes return a slice of 4 bytes with the mode in little endian +// encoding. +func (m FileMode) Bytes() []byte { + ret := make([]byte, 4) + binary.LittleEndian.PutUint32(ret, uint32(m)) + return ret +} + +// IsMalformed returns if the FileMode should not appear in a git packfile, +// this is: Empty and any other mode not mentioned as a constant in this +// package. +func (m FileMode) IsMalformed() bool { + return m != Dir && + m != Regular && + m != Deprecated && + m != Executable && + m != Symlink && + m != Submodule +} + +// String returns the FileMode as a string in the standard git format, +// this is, an octal number padded with ceros to 7 digits. Malformed +// modes are printed in that same format, for easier debugging. +// +// Example: Regular is "0100644", Empty is "0000000". +func (m FileMode) String() string { + return fmt.Sprintf("%07o", uint32(m)) +} + +// IsRegular returns if the FileMode represents that of a regular file, +// this is, either Regular or Deprecated. Please note that Executable +// are not regular even though in the UNIX tradition, they usually are: +// See the IsFile method. +func (m FileMode) IsRegular() bool { + return m == Regular || + m == Deprecated +} + +// IsFile returns if the FileMode represents that of a file, this is, +// Regular, Deprecated, Executable or Link. +func (m FileMode) IsFile() bool { + return m == Regular || + m == Deprecated || + m == Executable || + m == Symlink +} + +// ToOSFileMode returns the os.FileMode to be used when creating file +// system elements with the given git mode and a nil error on success. +// +// When the provided mode cannot be mapped to a valid file system mode +// (e.g. Submodule) it returns os.FileMode(0) and an error. +// +// The returned file mode does not take into account the umask. +func (m FileMode) ToOSFileMode() (os.FileMode, error) { + switch m { + case Dir: + return os.ModePerm | os.ModeDir, nil + case Submodule: + return os.ModePerm | os.ModeDir, nil + case Regular: + return os.FileMode(0644), nil + // Deprecated is no longer allowed: treated as a Regular instead + case Deprecated: + return os.FileMode(0644), nil + case Executable: + return os.FileMode(0755), nil + case Symlink: + return os.ModePerm | os.ModeSymlink, nil + } + + return os.FileMode(0), fmt.Errorf("malformed mode (%s)", m) +} diff --git a/models/filemode/filemode_test.go b/models/filemode/filemode_test.go new file mode 100644 index 00000000..02bf5c5c --- /dev/null +++ b/models/filemode/filemode_test.go @@ -0,0 +1,348 @@ +package filemode + +import ( + "os" + "testing" + + . "gopkg.in/check.v1" //nolint +) + +func Test(t *testing.T) { TestingT(t) } + +type ModeSuite struct{} + +var _ = Suite(&ModeSuite{}) + +func (s *ModeSuite) TestNew(c *C) { + for _, test := range [...]struct { + input string + expected FileMode + }{ + // these are the ones used in the packfile codification + // of the tree entries + {input: "40000", expected: Dir}, + {input: "100644", expected: Regular}, + {input: "100664", expected: Deprecated}, + {input: "100755", expected: Executable}, + {input: "120000", expected: Symlink}, + {input: "160000", expected: Submodule}, + // these are are not used by standard git to codify modes in + // packfiles, but they often appear when parsing some git + // outputs ("git diff-tree", for instance). + {input: "000000", expected: Empty}, + {input: "040000", expected: Dir}, + // these are valid inputs, but probably means there is a bug + // somewhere. + {input: "0", expected: Empty}, + {input: "42", expected: FileMode(042)}, + {input: "00000000000100644", expected: Regular}, + } { + comment := Commentf("input = %q", test.input) + obtained, err := New(test.input) + c.Assert(obtained, Equals, test.expected, comment) + c.Assert(err, IsNil, comment) + } +} + +func (s *ModeSuite) TestNewErrors(c *C) { + for _, input := range [...]string{ + "0x81a4", // Regular in hex + "-rw-r--r--", // Regular in default UNIX representation + "", + "-42", + "9", // this is no octal + "09", // looks like octal, but it is not + "mode", + "-100644", + "+100644", + } { + comment := Commentf("input = %q", input) + obtained, err := New(input) + c.Assert(obtained, Equals, Empty, comment) + c.Assert(err, Not(IsNil), comment) + } +} + +// fixtures for testing NewModeFromOSFileMode +type fixture struct { + input os.FileMode + expected FileMode + err string // error regexp, empty string for nil error +} + +func (f fixture) test(c *C) { + obtained, err := NewFromOSFileMode(f.input) + comment := Commentf("input = %s (%07o)", f.input, uint32(f.input)) + c.Assert(obtained, Equals, f.expected, comment) + if f.err != "" { + c.Assert(err, ErrorMatches, f.err, comment) + } else { + c.Assert(err, IsNil, comment) + } +} + +func (s *ModeSuite) TestNewFromOsFileModeSimplePerms(c *C) { + for _, f := range [...]fixture{ + {os.FileMode(0755) | os.ModeDir, Dir, ""}, // drwxr-xr-x + {os.FileMode(0700) | os.ModeDir, Dir, ""}, // drwx------ + {os.FileMode(0500) | os.ModeDir, Dir, ""}, // dr-x------ + {os.FileMode(0644), Regular, ""}, // -rw-r--r-- + {os.FileMode(0660), Regular, ""}, // -rw-rw---- + {os.FileMode(0640), Regular, ""}, // -rw-r----- + {os.FileMode(0600), Regular, ""}, // -rw------- + {os.FileMode(0400), Regular, ""}, // -r-------- + {os.FileMode(0000), Regular, ""}, // ---------- + {os.FileMode(0755), Executable, ""}, // -rwxr-xr-x + {os.FileMode(0700), Executable, ""}, // -rwx------ + {os.FileMode(0500), Executable, ""}, // -r-x------ + {os.FileMode(0744), Executable, ""}, // -rwxr--r-- + {os.FileMode(0540), Executable, ""}, // -r-xr----- + {os.FileMode(0550), Executable, ""}, // -r-xr-x--- + {os.FileMode(0777) | os.ModeSymlink, Symlink, ""}, // Lrwxrwxrwx + } { + f.test(c) + } +} + +func (s *ModeSuite) TestNewFromOsFileModeAppend(c *C) { + // append files are just regular files + fixture{ + input: os.FileMode(0644) | os.ModeAppend, // arw-r--r-- + expected: Regular, err: "", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeExclusive(c *C) { + // exclusive files are just regular or executable files + fixture{ + input: os.FileMode(0644) | os.ModeExclusive, // lrw-r--r-- + expected: Regular, err: "", + }.test(c) + + fixture{ + input: os.FileMode(0755) | os.ModeExclusive, // lrwxr-xr-x + expected: Executable, err: "", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeTemporary(c *C) { + // temporary files are ignored + fixture{ + input: os.FileMode(0644) | os.ModeTemporary, // Trw-r--r-- + expected: Empty, err: "no equivalent.*", + }.test(c) + + fixture{ + input: os.FileMode(0755) | os.ModeTemporary, // Trwxr-xr-x + expected: Empty, err: "no equivalent.*", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeDevice(c *C) { + // device files has no git equivalent + fixture{ + input: os.FileMode(0644) | os.ModeDevice, // Drw-r--r-- + expected: Empty, err: "no equivalent.*", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileNamedPipe(c *C) { + // named pipes files has not git equivalent + fixture{ + input: os.FileMode(0644) | os.ModeNamedPipe, // prw-r--r-- + expected: Empty, err: "no equivalent.*", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeSocket(c *C) { + // sockets has no git equivalent + fixture{ + input: os.FileMode(0644) | os.ModeSocket, // Srw-r--r-- + expected: Empty, err: "no equivalent.*", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeSetuid(c *C) { + // Setuid are just executables + fixture{ + input: os.FileMode(0755) | os.ModeSetuid, // urwxr-xr-x + expected: Executable, err: "", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeSetgid(c *C) { + // Setguid are regular or executables, depending on the owner perms + fixture{ + input: os.FileMode(0644) | os.ModeSetgid, // grw-r--r-- + expected: Regular, err: "", + }.test(c) + + fixture{ + input: os.FileMode(0755) | os.ModeSetgid, // grwxr-xr-x + expected: Executable, err: "", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeCharDevice(c *C) { + // char devices has no git equivalent + fixture{ + input: os.FileMode(0644) | os.ModeCharDevice, // crw-r--r-- + expected: Empty, err: "no equivalent.*", + }.test(c) +} + +func (s *ModeSuite) TestNewFromOsFileModeSticky(c *C) { + // dirs with the sticky bit are just dirs + fixture{ + input: os.FileMode(0755) | os.ModeDir | os.ModeSticky, // dtrwxr-xr-x + expected: Dir, err: "", + }.test(c) +} + +func (s *ModeSuite) TestByte(c *C) { + for _, test := range [...]struct { + input FileMode + expected []byte + }{ + {FileMode(0), []byte{0x00, 0x00, 0x00, 0x00}}, + {FileMode(1), []byte{0x01, 0x00, 0x00, 0x00}}, + {FileMode(15), []byte{0x0f, 0x00, 0x00, 0x00}}, + {FileMode(16), []byte{0x10, 0x00, 0x00, 0x00}}, + {FileMode(255), []byte{0xff, 0x00, 0x00, 0x00}}, + {FileMode(256), []byte{0x00, 0x01, 0x00, 0x00}}, + {Empty, []byte{0x00, 0x00, 0x00, 0x00}}, + {Dir, []byte{0x00, 0x40, 0x00, 0x00}}, + {Regular, []byte{0xa4, 0x81, 0x00, 0x00}}, + {Deprecated, []byte{0xb4, 0x81, 0x00, 0x00}}, + {Executable, []byte{0xed, 0x81, 0x00, 0x00}}, + {Symlink, []byte{0x00, 0xa0, 0x00, 0x00}}, + {Submodule, []byte{0x00, 0xe0, 0x00, 0x00}}, + } { + c.Assert(test.input.Bytes(), DeepEquals, test.expected, + Commentf("input = %s", test.input)) + } +} + +func (s *ModeSuite) TestIsMalformed(c *C) { + for _, test := range [...]struct { + mode FileMode + expected bool + }{ + {Empty, true}, + {Dir, false}, + {Regular, false}, + {Deprecated, false}, + {Executable, false}, + {Symlink, false}, + {Submodule, false}, + {FileMode(01), true}, + {FileMode(010), true}, + {FileMode(0100), true}, + {FileMode(01000), true}, + {FileMode(010000), true}, + {FileMode(0100000), true}, + } { + c.Assert(test.mode.IsMalformed(), Equals, test.expected) + } +} + +func (s *ModeSuite) TestString(c *C) { + for _, test := range [...]struct { + mode FileMode + expected string + }{ + {Empty, "0000000"}, + {Dir, "0040000"}, + {Regular, "0100644"}, + {Deprecated, "0100664"}, + {Executable, "0100755"}, + {Symlink, "0120000"}, + {Submodule, "0160000"}, + {FileMode(01), "0000001"}, + {FileMode(010), "0000010"}, + {FileMode(0100), "0000100"}, + {FileMode(01000), "0001000"}, + {FileMode(010000), "0010000"}, + {FileMode(0100000), "0100000"}, + } { + c.Assert(test.mode.String(), Equals, test.expected) + } +} + +func (s *ModeSuite) TestIsRegular(c *C) { + for _, test := range [...]struct { + mode FileMode + expected bool + }{ + {Empty, false}, + {Dir, false}, + {Regular, true}, + {Deprecated, true}, + {Executable, false}, + {Symlink, false}, + {Submodule, false}, + {FileMode(01), false}, + {FileMode(010), false}, + {FileMode(0100), false}, + {FileMode(01000), false}, + {FileMode(010000), false}, + {FileMode(0100000), false}, + } { + c.Assert(test.mode.IsRegular(), Equals, test.expected) + } +} + +func (s *ModeSuite) TestIsFile(c *C) { + for _, test := range [...]struct { + mode FileMode + expected bool + }{ + {Empty, false}, + {Dir, false}, + {Regular, true}, + {Deprecated, true}, + {Executable, true}, + {Symlink, true}, + {Submodule, false}, + {FileMode(01), false}, + {FileMode(010), false}, + {FileMode(0100), false}, + {FileMode(01000), false}, + {FileMode(010000), false}, + {FileMode(0100000), false}, + } { + c.Assert(test.mode.IsFile(), Equals, test.expected) + } +} + +func (s *ModeSuite) TestToOSFileMode(c *C) { + for _, test := range [...]struct { + input FileMode + expected os.FileMode + errRegExp string // empty string for nil error + }{ + {Empty, os.FileMode(0), "malformed.*"}, + {Dir, os.ModePerm | os.ModeDir, ""}, + {Regular, os.FileMode(0644), ""}, + {Deprecated, os.FileMode(0644), ""}, + {Executable, os.FileMode(0755), ""}, + {Symlink, os.ModePerm | os.ModeSymlink, ""}, + {Submodule, os.ModePerm | os.ModeDir, ""}, + {FileMode(01), os.FileMode(0), "malformed.*"}, + {FileMode(010), os.FileMode(0), "malformed.*"}, + {FileMode(0100), os.FileMode(0), "malformed.*"}, + {FileMode(01000), os.FileMode(0), "malformed.*"}, + {FileMode(010000), os.FileMode(0), "malformed.*"}, + {FileMode(0100000), os.FileMode(0), "malformed.*"}, + } { + obtained, err := test.input.ToOSFileMode() + comment := Commentf("input = %s", test.input) + if test.errRegExp != "" { + c.Assert(obtained, Equals, os.FileMode(0), comment) + c.Assert(err, ErrorMatches, test.errRegExp, comment) + } else { + c.Assert(obtained, Equals, test.expected, comment) + c.Assert(err, IsNil, comment) + } + } +} diff --git a/models/migrations/20210505110026_init_project.go b/models/migrations/20210505110026_init_project.go index e317fe68..6980d971 100644 --- a/models/migrations/20210505110026_init_project.go +++ b/models/migrations/20210505110026_init_project.go @@ -9,11 +9,13 @@ import ( func init() { Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + //common _, err := db.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`) if err != nil { return err } + //user _, err = db.NewCreateTable(). Model((*models.User)(nil)). Exec(ctx) @@ -26,6 +28,31 @@ func init() { Index("name_idx"). Column("name"). Exec(ctx) + if err != nil { + return err + } + //repository + _, err = db.NewCreateTable(). + Model((*models.Repository)(nil)). + Exec(ctx) + if err != nil { + return err + } + + //ref + _, err = db.NewCreateTable(). + Model((*models.Ref)(nil)). + Exec(ctx) + if err != nil { + return err + } + //object + _, err = db.NewCreateTable(). + Model((*models.Object)(nil)). + Exec(ctx) + if err != nil { + return err + } return err }, nil) } diff --git a/models/object.go b/models/object.go new file mode 100644 index 00000000..263bb913 --- /dev/null +++ b/models/object.go @@ -0,0 +1,114 @@ +package models + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/jiaozifs/jiaozifs/models/filemode" + "github.com/jiaozifs/jiaozifs/utils/hash" + "github.com/uptrace/bun" +) + +// ObjectType internal object type +// Integer values from 0 to 7 map to those exposed by git. +// AnyObject is used to represent any from 0 to 7. +type ObjectType int8 + +const ( + InvalidObject ObjectType = 0 + CommitObject ObjectType = 1 + TreeObject ObjectType = 2 + BlobObject ObjectType = 3 + TagObject ObjectType = 4 +) + +// Signature is used to identify who and when created a commit or tag. +type Signature struct { + // Name represents a person name. It is an arbitrary string. + Name string `bun:"name"` + // Email is an email, but it cannot be assumed to be well-formed. + Email string `bun:"email"` + // When is the timestamp of the signature. + When time.Time `bun:"when"` +} + +type TreeEntry struct { + Name string `bun:"name"` + Mode filemode.FileMode `bun:"mode"` + Hash hash.Hash `bun:"hash"` +} + +type Object struct { + bun.BaseModel `bun:"table:object"` + ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()"` + Hash hash.Hash `bun:"hash,type:bytea"` + Size int64 `bun:"size"` + //tree + SubObject []TreeEntry `bun:"subObj,type:jsonb"` + + //////********commit********//////// + // Author is the original author of the commit. + Author Signature `bun:"author,type:jsonb"` + // Committer is the one performing the commit, might be different from + // Author. + Committer Signature `bun:"committer,type:jsonb"` + // MergeTag is the embedded tag object when a merge commit is created by + // merging a signed tag. + MergeTag string `bun:"merge_tag"` //todo + // Message is the commit/tag message, contains arbitrary text. + Message string `bun:"message"` + // TreeHash is the hash of the root tree of the commit. + TreeHash hash.Hash `bun:"tree_hash,type:bytea"` + // ParentHashes are the hashes of the parent commits of the commit. + ParentHashes []hash.Hash `bun:"parent_hashes,type:bytea[]"` + + //////********commit********//////// + // Name of the tag. + Name string `bun:"name"` + // Tagger is the one who created the tag. + Tagger Signature `bun:"tagger,type:jsonb"` + // TargetType is the object type of the target. + TargetType ObjectType `bun:"target_type"` + // Target is the hash of the target object. + Target hash.Hash `bun:"target,type:bytea"` +} + +type IObjectRepo interface { + Insert(ctx context.Context, repo *Object) (*Object, error) + Get(ctx context.Context, id uuid.UUID) (*Object, error) + Count(ctx context.Context) (int, error) + List(ctx context.Context) ([]Object, error) +} + +var _ IObjectRepo = (*ObjectRepo)(nil) + +type ObjectRepo struct { + *bun.DB +} + +func NewObjectRepo(db *bun.DB) IObjectRepo { + return &ObjectRepo{db} +} + +func (o ObjectRepo) Insert(ctx context.Context, obj *Object) (*Object, error) { + _, err := o.DB.NewInsert().Model(obj).Exec(ctx) + if err != nil { + return nil, err + } + return obj, nil +} + +func (o ObjectRepo) Get(ctx context.Context, id uuid.UUID) (*Object, error) { + obj := &Object{} + return obj, o.DB.NewSelect().Model(obj).Where("id = ?", id).Scan(ctx) +} + +func (o ObjectRepo) Count(ctx context.Context) (int, error) { + return o.DB.NewSelect().Model((*Object)(nil)).Count(ctx) +} + +func (o ObjectRepo) List(ctx context.Context) ([]Object, error) { + obj := []Object{} + return obj, o.DB.NewSelect().Model(&obj).Scan(ctx) +} diff --git a/models/object_test.go b/models/object_test.go new file mode 100644 index 00000000..a7923575 --- /dev/null +++ b/models/object_test.go @@ -0,0 +1,39 @@ +package models_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/jiaozifs/jiaozifs/models" + "github.com/stretchr/testify/require" +) + +func TestObjectRepo_Insert(t *testing.T) { + ctx := context.Background() + postgres, db := setup(ctx, t) + defer postgres.Stop() //nolint + + repo := models.NewObjectRepo(db) + + objModel := &models.Object{} + require.NoError(t, gofakeit.Struct(objModel)) + newObj, err := repo.Insert(ctx, objModel) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, newObj.ID) + + count, err := repo.Count(ctx) + require.NoError(t, err) + require.Equal(t, 1, count) + + list, err := repo.List(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(list)) + + ref, err := repo.Get(ctx, newObj.ID) + require.NoError(t, err) + + require.True(t, cmp.Equal(newObj, ref, dbTimeCmpOpt)) +} diff --git a/models/ref.go b/models/ref.go new file mode 100644 index 00000000..889435be --- /dev/null +++ b/models/ref.go @@ -0,0 +1,54 @@ +package models + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type Ref struct { + bun.BaseModel `bun:"table:ref"` + ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()"` + // RepositoryId which repository this branch belong + RepositoryID uuid.UUID `bun:"repository_id,type:uuid,notnull"` + CommitHash uuid.UUID `bun:"commit_hash,type:uuid,notnull"` + // Path name/path of branch + Path string `bun:"path,notnull"` + // Description + Description string `bun:"description"` + // CreateId who create this branch + CreateID uuid.UUID `bun:"create_id,type:uuid,notnull"` + + CreatedAt time.Time `bun:"created_at"` + UpdatedAt time.Time `bun:"updated_at"` +} + +type IRefRepo interface { + Insert(ctx context.Context, repo *Ref) (*Ref, error) + Get(ctx context.Context, id uuid.UUID) (*Ref, error) +} + +var _ IRefRepo = (*RefRepo)(nil) + +type RefRepo struct { + *bun.DB +} + +func NewRefRepo(db *bun.DB) IRefRepo { + return &RefRepo{db} +} + +func (r RefRepo) Insert(ctx context.Context, ref *Ref) (*Ref, error) { + _, err := r.DB.NewInsert().Model(ref).Exec(ctx) + if err != nil { + return nil, err + } + return ref, nil +} + +func (r RefRepo) Get(ctx context.Context, id uuid.UUID) (*Ref, error) { + ref := &Ref{} + return ref, r.DB.NewSelect().Model(ref).Where("id = ?", id).Scan(ctx) +} diff --git a/models/ref_test.go b/models/ref_test.go new file mode 100644 index 00000000..8e78a565 --- /dev/null +++ b/models/ref_test.go @@ -0,0 +1,31 @@ +package models_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/jiaozifs/jiaozifs/models" + "github.com/stretchr/testify/require" +) + +func TestRefRepo_Insert(t *testing.T) { + ctx := context.Background() + postgres, db := setup(ctx, t) + defer postgres.Stop() //nolint + + repo := models.NewRefRepo(db) + + refModel := &models.Ref{} + require.NoError(t, gofakeit.Struct(refModel)) + newRef, err := repo.Insert(ctx, refModel) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, newRef.ID) + + ref, err := repo.Get(ctx, newRef.ID) + require.NoError(t, err) + + require.True(t, cmp.Equal(refModel, ref, dbTimeCmpOpt)) +} diff --git a/models/repository.go b/models/repository.go new file mode 100644 index 00000000..889efc12 --- /dev/null +++ b/models/repository.go @@ -0,0 +1,50 @@ +package models + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type Repository struct { + bun.BaseModel `bun:"table:repository"` + ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()"` + Name string `bun:"name,notnull"` + StorageNamespace string `bun:"storage_namespace,notnull"` + Description string `bun:"description"` + HEAD uuid.UUID `bun:"head,type:uuid,notnull"` + CreateID uuid.UUID `bun:"create_id,type:uuid,notnull"` + + CreatedAt time.Time `bun:"created_at"` + UpdatedAt time.Time `bun:"updated_at"` +} + +type IRepositoryRepo interface { + Insert(ctx context.Context, repo *Repository) (*Repository, error) + Get(ctx context.Context, id uuid.UUID) (*Repository, error) +} + +var _ IRepositoryRepo = (*RepositoryRepo)(nil) + +type RepositoryRepo struct { + *bun.DB +} + +func NewRepositoryRepo(db *bun.DB) IRepositoryRepo { + return &RepositoryRepo{db} +} + +func (r *RepositoryRepo) Insert(ctx context.Context, repo *Repository) (*Repository, error) { + _, err := r.DB.NewInsert().Model(repo).Exec(ctx) + if err != nil { + return nil, err + } + return repo, nil +} + +func (r *RepositoryRepo) Get(ctx context.Context, id uuid.UUID) (*Repository, error) { + repo := &Repository{} + return repo, r.DB.NewSelect().Model(repo).Where("id = ?", id).Scan(ctx) +} diff --git a/models/repository_test.go b/models/repository_test.go new file mode 100644 index 00000000..3b2e3e72 --- /dev/null +++ b/models/repository_test.go @@ -0,0 +1,31 @@ +package models_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/jiaozifs/jiaozifs/models" + "github.com/stretchr/testify/require" +) + +func TestRepositoryRepo_Insert(t *testing.T) { + ctx := context.Background() + postgres, db := setup(ctx, t) + defer postgres.Stop() //nolint + + repo := models.NewRepositoryRepo(db) + + repoModel := &models.Repository{} + require.NoError(t, gofakeit.Struct(repoModel)) + newUser, err := repo.Insert(ctx, repoModel) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, newUser.ID) + + user, err := repo.Get(ctx, newUser.ID) + require.NoError(t, err) + + require.True(t, cmp.Equal(repoModel, user, dbTimeCmpOpt)) +} diff --git a/models/user.go b/models/user.go index aa8a6123..87996a0b 100644 --- a/models/user.go +++ b/models/user.go @@ -10,7 +10,7 @@ import ( ) type User struct { - bun.BaseModel `bun:"table:users,alias:u"` + bun.BaseModel `bun:"table:users"` ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()"` Name string `bun:"name,notnull"` Email string `bun:"email,notnull"` @@ -24,7 +24,7 @@ type User struct { } type IUserRepo interface { - GetUser(ctx context.Context, id uuid.UUID) (*User, error) + Get(ctx context.Context, id uuid.UUID) (*User, error) Insert(ctx context.Context, user *User) (*User, error) } @@ -38,7 +38,7 @@ func NewUserRepo(db *bun.DB) IUserRepo { return &UserRepo{db} } -func (userRepo *UserRepo) GetUser(ctx context.Context, id uuid.UUID) (*User, error) { +func (userRepo *UserRepo) Get(ctx context.Context, id uuid.UUID) (*User, error) { user := &User{} return user, userRepo.DB.NewSelect().Model(user).Where("id = ?", id).Scan(ctx) } diff --git a/models/user_test.go b/models/user_test.go index 2fb49a46..40468bd3 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" @@ -43,28 +45,21 @@ func setup(ctx context.Context, t *testing.T) (*embeddedpostgres.EmbeddedPostgre require.NoError(t, err) return postgres, db } + func TestNewUserRepo(t *testing.T) { ctx := context.Background() postgres, db := setup(ctx, t) defer postgres.Stop() //nolint repo := models.NewUserRepo(db) - userModel := &models.User{ - Name: "aaa", - Email: "xx@gmail.com", - EncryptedPassword: "aaaaa", - CurrentSignInAt: time.Now(), - LastSignInAt: time.Now(), - CurrentSignInIP: "127.0.0.1", - LastSignInIP: "127.0.0.1", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + + userModel := &models.User{} + require.NoError(t, gofakeit.Struct(userModel)) newUser, err := repo.Insert(ctx, userModel) require.NoError(t, err) require.NotEqual(t, uuid.Nil, newUser.ID) - user, err := repo.GetUser(ctx, newUser.ID) + user, err := repo.Get(ctx, newUser.ID) require.NoError(t, err) require.True(t, cmp.Equal(userModel.UpdatedAt, user.UpdatedAt, dbTimeCmpOpt)) diff --git a/utils/hash/hash.go b/utils/hash/hash.go new file mode 100644 index 00000000..51beb812 --- /dev/null +++ b/utils/hash/hash.go @@ -0,0 +1,62 @@ +// Package hash provides a way for managing the +// underlying hash implementations used across go-git. +package hash + +import ( + "crypto" + "fmt" + "hash" + + "github.com/pjbgf/sha1cd" +) + +// algos is a map of hash algorithms. +var algos = map[crypto.Hash]func() hash.Hash{} + +func init() { + reset() +} + +// reset resets the default algos value. Can be used after running tests +// that registers new algorithms to avoid side effects. +func reset() { + algos[crypto.SHA1] = sha1cd.New + algos[crypto.SHA256] = crypto.SHA256.New +} + +// RegisterHasher allows for the hash algorithm used to be overridden. +// This ensures the hash selection for go-git must be explicit, when +// overriding the default value. +func RegisterHasher(h crypto.Hash, f func() hash.Hash) error { + if f == nil { + return fmt.Errorf("cannot register hash: f is nil") + } + + switch h { + case crypto.SHA1: + algos[h] = f + case crypto.SHA256: + algos[h] = f + default: + return fmt.Errorf("unsupported hash function: %v", h) + } + return nil +} + +type Hash []byte + +// Hasher is the same as hash.Hash. This allows consumers +// to not having to import this package alongside "hash". +type Hasher interface { + hash.Hash +} + +// New returns a new Hash for the given hash function. +// It panics if the hash function is not registered. +func New(h crypto.Hash) Hasher { + hh, ok := algos[h] + if !ok { + panic(fmt.Sprintf("hash algorithm not registered: %v", h)) + } + return hh() +} diff --git a/utils/hash/hash_sha1.go b/utils/hash/hash_sha1.go new file mode 100644 index 00000000..e3cb60fe --- /dev/null +++ b/utils/hash/hash_sha1.go @@ -0,0 +1,15 @@ +//go:build !sha256 +// +build !sha256 + +package hash + +import "crypto" + +const ( + // CryptoType defines what hash algorithm is being used. + CryptoType = crypto.SHA1 + // Size defines the amount of bytes the hash yields. + Size = 20 + // HexSize defines the strings size of the hash when represented in hexadecimal. + HexSize = 40 +) diff --git a/utils/hash/hash_sha256.go b/utils/hash/hash_sha256.go new file mode 100644 index 00000000..1c52b897 --- /dev/null +++ b/utils/hash/hash_sha256.go @@ -0,0 +1,15 @@ +//go:build sha256 +// +build sha256 + +package hash + +import "crypto" + +const ( + // CryptoType defines what hash algorithm is being used. + CryptoType = crypto.SHA256 + // Size defines the amount of bytes the hash yields. + Size = 32 + // HexSize defines the strings size of the hash when represented in hexadecimal. + HexSize = 64 +) diff --git a/utils/hash/hash_test.go b/utils/hash/hash_test.go new file mode 100644 index 00000000..cb049921 --- /dev/null +++ b/utils/hash/hash_test.go @@ -0,0 +1,103 @@ +package hash + +import ( + "crypto" + "crypto/sha1" + "crypto/sha512" + "encoding/hex" + "hash" + "strings" + "testing" +) + +func TestRegisterHash(t *testing.T) { + // Reset default hash to avoid side effects. + defer reset() + + tests := []struct { + name string + hash crypto.Hash + new func() hash.Hash + wantErr string + }{ + { + name: "sha1", + hash: crypto.SHA1, + new: sha1.New, + }, + { + name: "sha1", + hash: crypto.SHA1, + wantErr: "cannot register hash: f is nil", + }, + { + name: "sha512", + hash: crypto.SHA512, + new: sha512.New, + wantErr: "unsupported hash function", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := RegisterHasher(tt.hash, tt.new) + if tt.wantErr == "" && err != nil { + t.Errorf("unexpected error: %v", err) + } else if tt.wantErr != "" && err == nil { + t.Errorf("expected error: %v got: nil", tt.wantErr) + } else if err != nil && !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("expected error: %v got: %v", tt.wantErr, err) + } + }) + } +} + +// Verifies that the SHA1 implementation used is collision-resistant +// by default. +func TestSha1Collision(t *testing.T) { + defer reset() + + tests := []struct { + name string + content string + hash string + before func() + }{ + { + name: "sha-mbles-1: with collision detection", + content: "99040d047fe81780012000ff4b65792069732070617274206f66206120636f6c6c6973696f6e212049742773206120747261702179c61af0afcc054515d9274e7307624b1dc7fb23988bb8de8b575dba7b9eab31c1674b6d974378a827732ff5851c76a2e60772b5a47ce1eac40bb993c12d8c70e24a4f8d5fcdedc1b32c9cf19e31af2429759d42e4dfdb31719f587623ee552939b6dcdc459fca53553b70f87ede30a247ea3af6c759a2f20b320d760db64ff479084fd3ccb3cdd48362d96a9c430617caff6c36c637e53fde28417f626fec54ed7943a46e5f5730f2bb38fb1df6e0090010d00e24ad78bf92641993608e8d158a789f34c46fe1e6027f35a4cbfb827076c50eca0e8b7cca69bb2c2b790259f9bf9570dd8d4437a3115faff7c3cac09ad25266055c27104755178eaeff825a2caa2acfb5de64ce7641dc59a541a9fc9c756756e2e23dc713c8c24c9790aa6b0e38a7f55f14452a1ca2850ddd9562fd9a18ad42496aa97008f74672f68ef461eb88b09933d626b4f918749cc027fddd6c425fc4216835d0134d15285bab2cb784a4f7cbb4fb514d4bf0f6237cf00a9e9f132b9a066e6fd17f6c42987478586ff651af96747fb426b9872b9a88e4063f59bb334cc00650f83a80c42751b71974d300fc2819a2e8f1e32c1b51cb18e6bfc4db9baef675d4aaf5b1574a047f8f6dd2ec153a93412293974d928f88ced9363cfef97ce2e742bf34c96b8ef3875676fea5cca8e5f7dea0bab2413d4de00ee71ee01f162bdb6d1eafd925e6aebaae6a354ef17cf205a404fbdb12fc454d41fdd95cf2459664a2ad032d1da60a73264075d7f1e0d6c1403ae7a0d861df3fe5707188dd5e07d1589b9f8b6630553f8fc352b3e0c27da80bddba4c64020d", + hash: "4f3d9be4a472c4dae83c6314aa6c36a064c1fd14", + }, + { + name: "sha-mbles-1: with default SHA1", + content: "99040d047fe81780012000ff4b65792069732070617274206f66206120636f6c6c6973696f6e212049742773206120747261702179c61af0afcc054515d9274e7307624b1dc7fb23988bb8de8b575dba7b9eab31c1674b6d974378a827732ff5851c76a2e60772b5a47ce1eac40bb993c12d8c70e24a4f8d5fcdedc1b32c9cf19e31af2429759d42e4dfdb31719f587623ee552939b6dcdc459fca53553b70f87ede30a247ea3af6c759a2f20b320d760db64ff479084fd3ccb3cdd48362d96a9c430617caff6c36c637e53fde28417f626fec54ed7943a46e5f5730f2bb38fb1df6e0090010d00e24ad78bf92641993608e8d158a789f34c46fe1e6027f35a4cbfb827076c50eca0e8b7cca69bb2c2b790259f9bf9570dd8d4437a3115faff7c3cac09ad25266055c27104755178eaeff825a2caa2acfb5de64ce7641dc59a541a9fc9c756756e2e23dc713c8c24c9790aa6b0e38a7f55f14452a1ca2850ddd9562fd9a18ad42496aa97008f74672f68ef461eb88b09933d626b4f918749cc027fddd6c425fc4216835d0134d15285bab2cb784a4f7cbb4fb514d4bf0f6237cf00a9e9f132b9a066e6fd17f6c42987478586ff651af96747fb426b9872b9a88e4063f59bb334cc00650f83a80c42751b71974d300fc2819a2e8f1e32c1b51cb18e6bfc4db9baef675d4aaf5b1574a047f8f6dd2ec153a93412293974d928f88ced9363cfef97ce2e742bf34c96b8ef3875676fea5cca8e5f7dea0bab2413d4de00ee71ee01f162bdb6d1eafd925e6aebaae6a354ef17cf205a404fbdb12fc454d41fdd95cf2459664a2ad032d1da60a73264075d7f1e0d6c1403ae7a0d861df3fe5707188dd5e07d1589b9f8b6630553f8fc352b3e0c27da80bddba4c64020d", + hash: "8ac60ba76f1999a1ab70223f225aefdc78d4ddc0", + before: func() { + _ = RegisterHasher(crypto.SHA1, sha1.New) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + h := New(crypto.SHA1) + data, err := hex.DecodeString(tt.content) + if err != nil { + t.Fatal(err) + } + + h.Reset() + h.Write(data) + sum := h.Sum(nil) + got := hex.EncodeToString(sum) + + if tt.hash != got { + t.Errorf("\n got: %q\nwanted: %q", got, tt.hash) + } + }) + } +}