satellite/{db, admin}: added endpoints to update user's and project's user_agent
Added backend (for now) implementation for updating user's and projects's user_agent using admin API. Updating both user and project also updates bucket_metainfo and value_attribution tables. Issue: https://github.com/storj/storj-private/issues/297 Change-Id: I40244bbaa08b46834c1b1d0720e7d84d0c2a0330
This commit is contained in:
parent
fe0c3a13b4
commit
1eee2fad69
2
go.mod
2
go.mod
@ -44,6 +44,7 @@ require (
|
||||
github.com/zeebo/blake3 v0.2.3
|
||||
github.com/zeebo/clingy v0.0.0-20230602044025-906be850f10d
|
||||
github.com/zeebo/errs v1.3.0
|
||||
github.com/zeebo/errs/v2 v2.0.3
|
||||
github.com/zeebo/ini v0.0.0-20210514163846-cc8fbd8d9599
|
||||
github.com/zyedidia/generic v1.2.1
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
@ -124,7 +125,6 @@ require (
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect
|
||||
github.com/zeebo/admission/v3 v3.0.3 // indirect
|
||||
github.com/zeebo/errs/v2 v2.0.3 // indirect
|
||||
github.com/zeebo/float16 v0.1.0 // indirect
|
||||
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 // indirect
|
||||
github.com/zeebo/mwc v0.0.4 // indirect
|
||||
|
@ -4,6 +4,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
@ -15,6 +16,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/zeebo/errs/v2"
|
||||
|
||||
"storj.io/common/macaroon"
|
||||
"storj.io/common/memory"
|
||||
@ -440,6 +442,82 @@ func (server *Server) renameProject(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) updateProjectsUserAgent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
projectUUIDString, ok := vars["project"]
|
||||
if !ok {
|
||||
sendJSONError(w, "project-uuid missing",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
projectUUID, err := uuid.FromString(projectUUIDString)
|
||||
if err != nil {
|
||||
sendJSONError(w, "invalid project-uuid",
|
||||
err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
sendJSONError(w, "project with specified uuid does not exist",
|
||||
"", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sendJSONError(w, "error getting project",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
creationDatePlusMonth := project.CreatedAt.AddDate(0, 1, 0)
|
||||
if time.Now().After(creationDatePlusMonth) {
|
||||
sendJSONError(w, "this project was created more than a month ago",
|
||||
"we should update user agent only for recently created projects", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to read body",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &input)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to unmarshal request",
|
||||
err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if input.UserAgent == "" {
|
||||
sendJSONError(w, "UserAgent was not provided",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newUserAgent := []byte(input.UserAgent)
|
||||
|
||||
if bytes.Equal(project.UserAgent, newUserAgent) {
|
||||
sendJSONError(w, "new UserAgent is equal to existing projects UserAgent",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = server._updateProjectsUserAgent(ctx, project.ID, newUserAgent)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to update projects user agent",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@ -502,6 +580,45 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) _updateProjectsUserAgent(ctx context.Context, projectID uuid.UUID, newUserAgent []byte) (err error) {
|
||||
err = server.db.Console().Projects().UpdateUserAgent(ctx, projectID, newUserAgent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listOptions := buckets.ListOptions{
|
||||
Direction: buckets.DirectionForward,
|
||||
}
|
||||
|
||||
allowedBuckets := macaroon.AllowedBuckets{
|
||||
All: true,
|
||||
}
|
||||
|
||||
projectBuckets, err := server.db.Buckets().ListBuckets(ctx, projectID, listOptions, allowedBuckets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errList errs.Group
|
||||
for _, bucket := range projectBuckets.Items {
|
||||
err = server.db.Buckets().UpdateUserAgent(ctx, projectID, bucket.Name, newUserAgent)
|
||||
if err != nil {
|
||||
errList.Append(err)
|
||||
}
|
||||
|
||||
err = server.db.Attribution().UpdateUserAgent(ctx, projectID, bucket.Name, newUserAgent)
|
||||
if err != nil {
|
||||
errList.Append(err)
|
||||
}
|
||||
}
|
||||
|
||||
if errList.Err() != nil {
|
||||
return errList.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) checkInvoicing(ctx context.Context, w http.ResponseWriter, projectID uuid.UUID) (openInvoices bool) {
|
||||
year, month, _ := server.nowFn().UTC().Date()
|
||||
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/attribution"
|
||||
"storj.io/storj/satellite/buckets"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/payments/stripe"
|
||||
@ -244,6 +245,109 @@ func TestProjectAdd(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateProjectsUserAgent(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
StorageNodeCount: 0,
|
||||
UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(_ *zap.Logger, _ int, config *satellite.Config) {
|
||||
config.Admin.Address = "127.0.0.1:0"
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
db := planet.Satellites[0].DB
|
||||
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
|
||||
project := planet.Uplinks[0].Projects[0]
|
||||
newUserAgent := "awesome user agent value"
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
bucketName := "testName"
|
||||
bucketName1 := "testName1"
|
||||
|
||||
bucketID, err := uuid.New()
|
||||
require.NoError(t, err)
|
||||
bucketID1, err := uuid.New()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Buckets().CreateBucket(ctx, buckets.Bucket{
|
||||
ID: bucketID,
|
||||
Name: bucketName,
|
||||
ProjectID: project.ID,
|
||||
UserAgent: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Buckets().CreateBucket(ctx, buckets.Bucket{
|
||||
ID: bucketID1,
|
||||
Name: bucketName1,
|
||||
ProjectID: project.ID,
|
||||
UserAgent: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Attribution().Insert(ctx, &attribution.Info{
|
||||
ProjectID: project.ID,
|
||||
BucketName: []byte(bucketName),
|
||||
UserAgent: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Attribution().Insert(ctx, &attribution.Info{
|
||||
ProjectID: project.ID,
|
||||
BucketName: []byte(bucketName1),
|
||||
UserAgent: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
body := strings.NewReader(fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("http://"+address.String()+"/api/projects/%s/useragent", project.ID.String()), body)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)
|
||||
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.NoError(t, response.Body.Close())
|
||||
|
||||
newUserAgentBytes := []byte(newUserAgent)
|
||||
|
||||
updatedProject, err := db.Console().Projects().Get(ctx, project.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newUserAgentBytes, updatedProject.UserAgent)
|
||||
|
||||
projectBuckets, err := planet.Satellites[0].API.Buckets.Service.ListBuckets(ctx, updatedProject.ID, buckets.ListOptions{Direction: buckets.DirectionForward}, macaroon.AllowedBuckets{All: true})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(projectBuckets.Items))
|
||||
|
||||
for _, bucket := range projectBuckets.Items {
|
||||
require.Equal(t, newUserAgentBytes, bucket.UserAgent)
|
||||
}
|
||||
|
||||
bucketAttribution, err := db.Attribution().Get(ctx, project.ID, []byte(bucketName))
|
||||
require.Equal(t, newUserAgentBytes, bucketAttribution.UserAgent)
|
||||
require.NoError(t, err)
|
||||
|
||||
bucketAttribution1, err := db.Attribution().Get(ctx, project.ID, []byte(bucketName1))
|
||||
require.Equal(t, newUserAgentBytes, bucketAttribution1.UserAgent)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Same UserAgent", func(t *testing.T) {
|
||||
newProject, err := db.Console().Projects().Insert(ctx, &console.Project{
|
||||
Name: "test",
|
||||
UserAgent: []byte(newUserAgent),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
link := fmt.Sprintf("http://"+address.String()+"/api/projects/%s/useragent", newProject.ID)
|
||||
body := fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent)
|
||||
responseBody := assertReq(ctx, t, link, http.MethodPatch, body, http.StatusBadRequest, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(responseBody), "new UserAgent is equal to existing projects UserAgent")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestProjectRename(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"storj.io/common/errs2"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
adminui "storj.io/storj/satellite/admin/ui"
|
||||
"storj.io/storj/satellite/attribution"
|
||||
"storj.io/storj/satellite/buckets"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleweb"
|
||||
@ -64,6 +65,10 @@ type DB interface {
|
||||
OIDC() oidc.DB
|
||||
// StripeCoinPayments returns database for satellite stripe coin payments
|
||||
StripeCoinPayments() stripe.DB
|
||||
// Buckets returns database for buckets metainfo.
|
||||
Buckets() buckets.DB
|
||||
// Attribution returns database for value attribution.
|
||||
Attribution() attribution.DB
|
||||
}
|
||||
|
||||
// Server provides endpoints for administrative tasks.
|
||||
@ -117,6 +122,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
|
||||
fullAccessAPI.HandleFunc("/users/{useremail}", server.updateUser).Methods("PUT")
|
||||
fullAccessAPI.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE")
|
||||
fullAccessAPI.HandleFunc("/users/{useremail}/mfa", server.disableUserMFA).Methods("DELETE")
|
||||
fullAccessAPI.HandleFunc("/users/{useremail}/useragent", server.updateUsersUserAgent).Methods("PATCH")
|
||||
fullAccessAPI.HandleFunc("/oauth/clients", server.createOAuthClient).Methods("POST")
|
||||
fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.updateOAuthClient).Methods("PUT")
|
||||
fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.deleteOAuthClient).Methods("DELETE")
|
||||
@ -131,6 +137,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
|
||||
fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.createGeofenceForBucket).Methods("POST")
|
||||
fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.deleteGeofenceForBucket).Methods("DELETE")
|
||||
fullAccessAPI.HandleFunc("/projects/{project}/usage", server.checkProjectUsage).Methods("GET")
|
||||
fullAccessAPI.HandleFunc("/projects/{project}/useragent", server.updateProjectsUserAgent).Methods("PATCH")
|
||||
fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.getAPIKey).Methods("GET")
|
||||
fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.deleteAPIKey).Methods("DELETE")
|
||||
fullAccessAPI.HandleFunc("/restkeys/{useremail}", server.addRESTKey).Methods("POST")
|
||||
|
@ -4,6 +4,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@ -11,8 +12,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zeebo/errs"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"storj.io/common/memory"
|
||||
@ -343,6 +346,101 @@ func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) updateUsersUserAgent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userEmail, ok := vars["useremail"]
|
||||
if !ok {
|
||||
sendJSONError(w, "user-email missing",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := server.db.Console().Users().GetByEmail(ctx, userEmail)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail),
|
||||
"", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to get user",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
creationDatePlusMonth := user.CreatedAt.AddDate(0, 1, 0)
|
||||
if time.Now().After(creationDatePlusMonth) {
|
||||
sendJSONError(w, "this user was created more than a month ago",
|
||||
"we should update user agent only for recently created users", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to read body",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &input)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to unmarshal request",
|
||||
err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if input.UserAgent == "" {
|
||||
sendJSONError(w, "UserAgent was not provided",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newUserAgent := []byte(input.UserAgent)
|
||||
|
||||
if bytes.Equal(user.UserAgent, newUserAgent) {
|
||||
sendJSONError(w, "new UserAgent is equal to existing users UserAgent",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = server.db.Console().Users().UpdateUserAgent(ctx, user.ID, newUserAgent)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to update user's user agent",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
projects, err := server.db.Console().Projects().GetOwn(ctx, user.ID)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to get users projects",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var errList errs.Group
|
||||
for _, project := range projects {
|
||||
if bytes.Equal(project.UserAgent, newUserAgent) {
|
||||
errList.Add(errs.New("projectID: %s. New UserAgent is equal to existing users UserAgent", project.ID))
|
||||
continue
|
||||
}
|
||||
|
||||
err = server._updateProjectsUserAgent(ctx, project.ID, newUserAgent)
|
||||
if err != nil {
|
||||
errList.Add(errs.New("projectID: %s. Failed to update projects user agent: %s", project.ID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if errList.Err() != nil {
|
||||
sendJSONError(w, "failed to update projects user agent",
|
||||
errList.Err().Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// updateLimits updates user limits and all project limits for that user (future and existing).
|
||||
func (server *Server) updateLimits(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
@ -272,6 +272,56 @@ func TestUserUpdate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateUsersUserAgent(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
StorageNodeCount: 0,
|
||||
UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(_ *zap.Logger, _ int, config *satellite.Config) {
|
||||
config.Admin.Address = "127.0.0.1:0"
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
db := planet.Satellites[0].DB
|
||||
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
|
||||
project := planet.Uplinks[0].Projects[0]
|
||||
newUserAgent := "awesome user agent value"
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
body := strings.NewReader(fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("http://"+address.String()+"/api/users/%s/useragent", project.Owner.Email), body)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)
|
||||
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.NoError(t, response.Body.Close())
|
||||
|
||||
newUserAgentBytes := []byte(newUserAgent)
|
||||
|
||||
updatedUser, err := db.Console().Users().Get(ctx, project.Owner.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newUserAgentBytes, updatedUser.UserAgent)
|
||||
|
||||
updatedProject, err := db.Console().Projects().Get(ctx, project.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newUserAgentBytes, updatedProject.UserAgent)
|
||||
})
|
||||
|
||||
t.Run("Same UserAgent", func(t *testing.T) {
|
||||
err := db.Console().Users().UpdateUserAgent(ctx, project.Owner.ID, []byte(newUserAgent))
|
||||
require.NoError(t, err)
|
||||
|
||||
link := fmt.Sprintf("http://"+address.String()+"/api/users/%s/useragent", project.Owner.Email)
|
||||
body := fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent)
|
||||
responseBody := assertReq(ctx, t, link, http.MethodPatch, body, http.StatusBadRequest, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(responseBody), "new UserAgent is equal to existing users UserAgent")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisableMFA(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
|
@ -44,6 +44,8 @@ type DB interface {
|
||||
Get(ctx context.Context, projectID uuid.UUID, bucketName []byte) (*Info, error)
|
||||
// Insert creates and stores new Info.
|
||||
Insert(ctx context.Context, info *Info) (*Info, error)
|
||||
// UpdateUserAgent updates bucket attribution data.
|
||||
UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) error
|
||||
// QueryAttribution queries partner bucket attribution data.
|
||||
QueryAttribution(ctx context.Context, userAgent []byte, start time.Time, end time.Time) ([]*BucketUsage, error)
|
||||
// QueryAllAttribution queries all partner bucket usage data.
|
||||
|
@ -101,6 +101,8 @@ type DB interface {
|
||||
GetBucketID(ctx context.Context, bucket metabase.BucketLocation) (id uuid.UUID, err error)
|
||||
// UpdateBucket updates an existing bucket
|
||||
UpdateBucket(ctx context.Context, bucket Bucket) (_ Bucket, err error)
|
||||
// UpdateUserAgent updates buckets user agent.
|
||||
UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) error
|
||||
// DeleteBucket deletes a bucket
|
||||
DeleteBucket(ctx context.Context, bucketName []byte, projectID uuid.UUID) (err error)
|
||||
// ListBuckets returns all buckets for a project
|
||||
|
@ -55,6 +55,9 @@ type Projects interface {
|
||||
|
||||
// UpdateUsageLimits is a method for updating project's usage limits.
|
||||
UpdateUsageLimits(ctx context.Context, id uuid.UUID, limits UsageLimits) error
|
||||
|
||||
// UpdateUserAgent is a method for updating projects user agent.
|
||||
UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) error
|
||||
}
|
||||
|
||||
// UsageLimitsConfig is a configuration struct for default per-project usage limits.
|
||||
|
@ -42,6 +42,8 @@ type Users interface {
|
||||
Update(ctx context.Context, userID uuid.UUID, request UpdateUserRequest) error
|
||||
// UpdatePaidTier sets whether the user is in the paid tier.
|
||||
UpdatePaidTier(ctx context.Context, id uuid.UUID, paidTier bool, projectBandwidthLimit, projectStorageLimit memory.Size, projectSegmentLimit int64, projectLimit int) error
|
||||
// UpdateUserAgent is a method to update the user's user agent.
|
||||
UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) error
|
||||
// UpdateUserProjectLimits is a method to update the user's usage limits for new projects.
|
||||
UpdateUserProjectLimits(ctx context.Context, id uuid.UUID, limits UsageLimits) error
|
||||
// GetProjectLimit is a method to get the users project limit
|
||||
|
@ -243,6 +243,20 @@ func (keys *attributionDB) Get(ctx context.Context, projectID uuid.UUID, bucketN
|
||||
return attributionFromDBX(dbxInfo)
|
||||
}
|
||||
|
||||
// UpdateUserAgent updates bucket attribution data.
|
||||
func (keys *attributionDB) UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
_, err = keys.db.Update_ValueAttribution_By_ProjectId_And_BucketName(ctx,
|
||||
dbx.ValueAttribution_ProjectId(projectID[:]),
|
||||
dbx.ValueAttribution_BucketName([]byte(bucketName)),
|
||||
dbx.ValueAttribution_Update_Fields{
|
||||
UserAgent: dbx.ValueAttribution_UserAgent(userAgent),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert implements create partner info.
|
||||
func (keys *attributionDB) Insert(ctx context.Context, info *attribution.Info) (_ *attribution.Info, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -168,6 +168,20 @@ func (db *bucketsDB) UpdateBucket(ctx context.Context, bucket buckets.Bucket) (_
|
||||
return convertDBXtoBucket(dbxBucket)
|
||||
}
|
||||
|
||||
// UpdateUserAgent updates buckets user agent.
|
||||
func (db *bucketsDB) UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
_, err = db.db.Update_BucketMetainfo_By_ProjectId_And_Name(ctx,
|
||||
dbx.BucketMetainfo_ProjectId(projectID[:]),
|
||||
dbx.BucketMetainfo_Name([]byte(bucketName)),
|
||||
dbx.BucketMetainfo_Update_Fields{
|
||||
UserAgent: dbx.BucketMetainfo_UserAgent(userAgent),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteBucket deletes a bucket.
|
||||
func (db *bucketsDB) DeleteBucket(ctx context.Context, bucketName []byte, projectID uuid.UUID) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -40,7 +40,7 @@ model project (
|
||||
// max_buckets is the maximum number of buckets that can be created for the project.
|
||||
field max_buckets int ( nullable, updatable )
|
||||
// user_agent is the referred partner who created the project.
|
||||
field user_agent blob ( nullable )
|
||||
field user_agent blob ( nullable, updatable )
|
||||
// owner_id refers to the user UUID in user.id.
|
||||
field owner_id blob
|
||||
// salt is used for salting the user passphrase for the content.
|
||||
|
@ -148,7 +148,7 @@ model value_attribution (
|
||||
// user_agent is the first User-Agent that was used to upload data.
|
||||
// unless the user signed up with a specific partner.
|
||||
// note: this field is duplicated in bucket_metainfo.user_agent.
|
||||
field user_agent blob ( nullable )
|
||||
field user_agent blob ( updatable, nullable )
|
||||
// TODO remove as part of release 1.81 or after
|
||||
field partner_id blob (nullable, default null)
|
||||
// last_updated is updated whenever the row changes.
|
||||
@ -156,6 +156,10 @@ model value_attribution (
|
||||
)
|
||||
|
||||
create value_attribution ()
|
||||
update value_attribution (
|
||||
where value_attribution.project_id = ?
|
||||
where value_attribution.bucket_name = ?
|
||||
)
|
||||
|
||||
read one (
|
||||
select value_attribution
|
||||
|
@ -5573,6 +5573,7 @@ type Project_Update_Fields struct {
|
||||
RateLimit Project_RateLimit_Field
|
||||
BurstLimit Project_BurstLimit_Field
|
||||
MaxBuckets Project_MaxBuckets_Field
|
||||
UserAgent Project_UserAgent_Field
|
||||
DefaultPlacement Project_DefaultPlacement_Field
|
||||
}
|
||||
|
||||
@ -9335,6 +9336,7 @@ type User_Update_Fields struct {
|
||||
ShortName User_ShortName_Field
|
||||
PasswordHash User_PasswordHash_Field
|
||||
Status User_Status_Field
|
||||
UserAgent User_UserAgent_Field
|
||||
ProjectLimit User_ProjectLimit_Field
|
||||
ProjectBandwidthLimit User_ProjectBandwidthLimit_Field
|
||||
ProjectStorageLimit User_ProjectStorageLimit_Field
|
||||
@ -10313,6 +10315,7 @@ type ValueAttribution_Create_Fields struct {
|
||||
}
|
||||
|
||||
type ValueAttribution_Update_Fields struct {
|
||||
UserAgent ValueAttribution_UserAgent_Field
|
||||
}
|
||||
|
||||
type ValueAttribution_ProjectId_Field struct {
|
||||
@ -18304,6 +18307,11 @@ func (obj *pgxImpl) Update_Project_By_Id(ctx context.Context,
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("max_buckets = ?"))
|
||||
}
|
||||
|
||||
if update.UserAgent._set {
|
||||
__values = append(__values, update.UserAgent.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?"))
|
||||
}
|
||||
|
||||
if update.DefaultPlacement._set {
|
||||
__values = append(__values, update.DefaultPlacement.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("default_placement = ?"))
|
||||
@ -18461,6 +18469,49 @@ func (obj *pgxImpl) Update_BucketMetainfo_By_ProjectId_And_Name(ctx context.Cont
|
||||
return bucket_metainfo, nil
|
||||
}
|
||||
|
||||
func (obj *pgxImpl) Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context,
|
||||
value_attribution_project_id ValueAttribution_ProjectId_Field,
|
||||
value_attribution_bucket_name ValueAttribution_BucketName_Field,
|
||||
update ValueAttribution_Update_Fields) (
|
||||
value_attribution *ValueAttribution, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
var __sets = &__sqlbundle_Hole{}
|
||||
|
||||
var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("UPDATE value_attributions SET "), __sets, __sqlbundle_Literal(" WHERE value_attributions.project_id = ? AND value_attributions.bucket_name = ? RETURNING value_attributions.project_id, value_attributions.bucket_name, value_attributions.user_agent, value_attributions.partner_id, value_attributions.last_updated")}}
|
||||
|
||||
__sets_sql := __sqlbundle_Literals{Join: ", "}
|
||||
var __values []interface{}
|
||||
var __args []interface{}
|
||||
|
||||
if update.UserAgent._set {
|
||||
__values = append(__values, update.UserAgent.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?"))
|
||||
}
|
||||
|
||||
__now := obj.db.Hooks.Now().UTC()
|
||||
|
||||
__values = append(__values, __now)
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("last_updated = ?"))
|
||||
|
||||
__args = append(__args, value_attribution_project_id.value(), value_attribution_bucket_name.value())
|
||||
|
||||
__values = append(__values, __args...)
|
||||
__sets.SQL = __sets_sql
|
||||
|
||||
var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt)
|
||||
obj.logStmt(__stmt, __values...)
|
||||
|
||||
value_attribution = &ValueAttribution{}
|
||||
err = obj.queryRowContext(ctx, __stmt, __values...).Scan(&value_attribution.ProjectId, &value_attribution.BucketName, &value_attribution.UserAgent, &value_attribution.PartnerId, &value_attribution.LastUpdated)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, obj.makeErr(err)
|
||||
}
|
||||
return value_attribution, nil
|
||||
}
|
||||
|
||||
func (obj *pgxImpl) Update_User_By_Id(ctx context.Context,
|
||||
user_id User_Id_Field,
|
||||
update User_Update_Fields) (
|
||||
@ -18504,6 +18555,11 @@ func (obj *pgxImpl) Update_User_By_Id(ctx context.Context,
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("status = ?"))
|
||||
}
|
||||
|
||||
if update.UserAgent._set {
|
||||
__values = append(__values, update.UserAgent.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?"))
|
||||
}
|
||||
|
||||
if update.ProjectLimit._set {
|
||||
__values = append(__values, update.ProjectLimit.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("project_limit = ?"))
|
||||
@ -26211,6 +26267,11 @@ func (obj *pgxcockroachImpl) Update_Project_By_Id(ctx context.Context,
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("max_buckets = ?"))
|
||||
}
|
||||
|
||||
if update.UserAgent._set {
|
||||
__values = append(__values, update.UserAgent.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?"))
|
||||
}
|
||||
|
||||
if update.DefaultPlacement._set {
|
||||
__values = append(__values, update.DefaultPlacement.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("default_placement = ?"))
|
||||
@ -26368,6 +26429,49 @@ func (obj *pgxcockroachImpl) Update_BucketMetainfo_By_ProjectId_And_Name(ctx con
|
||||
return bucket_metainfo, nil
|
||||
}
|
||||
|
||||
func (obj *pgxcockroachImpl) Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context,
|
||||
value_attribution_project_id ValueAttribution_ProjectId_Field,
|
||||
value_attribution_bucket_name ValueAttribution_BucketName_Field,
|
||||
update ValueAttribution_Update_Fields) (
|
||||
value_attribution *ValueAttribution, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
var __sets = &__sqlbundle_Hole{}
|
||||
|
||||
var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("UPDATE value_attributions SET "), __sets, __sqlbundle_Literal(" WHERE value_attributions.project_id = ? AND value_attributions.bucket_name = ? RETURNING value_attributions.project_id, value_attributions.bucket_name, value_attributions.user_agent, value_attributions.partner_id, value_attributions.last_updated")}}
|
||||
|
||||
__sets_sql := __sqlbundle_Literals{Join: ", "}
|
||||
var __values []interface{}
|
||||
var __args []interface{}
|
||||
|
||||
if update.UserAgent._set {
|
||||
__values = append(__values, update.UserAgent.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?"))
|
||||
}
|
||||
|
||||
__now := obj.db.Hooks.Now().UTC()
|
||||
|
||||
__values = append(__values, __now)
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("last_updated = ?"))
|
||||
|
||||
__args = append(__args, value_attribution_project_id.value(), value_attribution_bucket_name.value())
|
||||
|
||||
__values = append(__values, __args...)
|
||||
__sets.SQL = __sets_sql
|
||||
|
||||
var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt)
|
||||
obj.logStmt(__stmt, __values...)
|
||||
|
||||
value_attribution = &ValueAttribution{}
|
||||
err = obj.queryRowContext(ctx, __stmt, __values...).Scan(&value_attribution.ProjectId, &value_attribution.BucketName, &value_attribution.UserAgent, &value_attribution.PartnerId, &value_attribution.LastUpdated)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, obj.makeErr(err)
|
||||
}
|
||||
return value_attribution, nil
|
||||
}
|
||||
|
||||
func (obj *pgxcockroachImpl) Update_User_By_Id(ctx context.Context,
|
||||
user_id User_Id_Field,
|
||||
update User_Update_Fields) (
|
||||
@ -26411,6 +26515,11 @@ func (obj *pgxcockroachImpl) Update_User_By_Id(ctx context.Context,
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("status = ?"))
|
||||
}
|
||||
|
||||
if update.UserAgent._set {
|
||||
__values = append(__values, update.UserAgent.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?"))
|
||||
}
|
||||
|
||||
if update.ProjectLimit._set {
|
||||
__values = append(__values, update.ProjectLimit.value())
|
||||
__sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("project_limit = ?"))
|
||||
@ -29786,6 +29895,18 @@ func (rx *Rx) Update_User_By_Id(ctx context.Context,
|
||||
return tx.Update_User_By_Id(ctx, user_id, update)
|
||||
}
|
||||
|
||||
func (rx *Rx) Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context,
|
||||
value_attribution_project_id ValueAttribution_ProjectId_Field,
|
||||
value_attribution_bucket_name ValueAttribution_BucketName_Field,
|
||||
update ValueAttribution_Update_Fields) (
|
||||
value_attribution *ValueAttribution, err error) {
|
||||
var tx *Tx
|
||||
if tx, err = rx.getTx(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
return tx.Update_ValueAttribution_By_ProjectId_And_BucketName(ctx, value_attribution_project_id, value_attribution_bucket_name, update)
|
||||
}
|
||||
|
||||
func (rx *Rx) Update_WebappSession_By_Id(ctx context.Context,
|
||||
webapp_session_id WebappSession_Id_Field,
|
||||
update WebappSession_Update_Fields) (
|
||||
@ -30713,6 +30834,12 @@ type Methods interface {
|
||||
update User_Update_Fields) (
|
||||
user *User, err error)
|
||||
|
||||
Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context,
|
||||
value_attribution_project_id ValueAttribution_ProjectId_Field,
|
||||
value_attribution_bucket_name ValueAttribution_BucketName_Field,
|
||||
update ValueAttribution_Update_Fields) (
|
||||
value_attribution *ValueAttribution, err error)
|
||||
|
||||
Update_WebappSession_By_Id(ctx context.Context,
|
||||
webapp_session_id WebappSession_Id_Field,
|
||||
update WebappSession_Update_Fields) (
|
||||
|
@ -26,7 +26,7 @@ model user (
|
||||
// status indicates whether the user is inactive=0, active=1, or deleted=2. See console.UserStatus for details.
|
||||
field status int ( updatable, autoinsert )
|
||||
// user_agent contains the partner parameter from registration.
|
||||
field user_agent blob ( nullable )
|
||||
field user_agent blob ( updatable, nullable )
|
||||
// created_at indicates when the user was created.
|
||||
field created_at timestamp ( autoinsert )
|
||||
|
||||
|
@ -291,6 +291,19 @@ func (projects *projects) UpdateBucketLimit(ctx context.Context, id uuid.UUID, n
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateUserAgent is a method for updating projects user agent.
|
||||
func (projects *projects) UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
_, err = projects.db.Update_Project_By_Id(ctx,
|
||||
dbx.Project_Id(id[:]),
|
||||
dbx.Project_Update_Fields{
|
||||
UserAgent: dbx.Project_UserAgent(userAgent),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List returns paginated projects, created before provided timestamp.
|
||||
func (projects *projects) List(ctx context.Context, offset int64, limit int, before time.Time) (_ console.ProjectsPage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -305,6 +305,21 @@ func (users *users) UpdatePaidTier(ctx context.Context, id uuid.UUID, paidTier b
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateUserAgent is a method to update the user's user agent.
|
||||
func (users *users) UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
_, err = users.db.Update_User_By_Id(
|
||||
ctx,
|
||||
dbx.User_Id(id[:]),
|
||||
dbx.User_Update_Fields{
|
||||
UserAgent: dbx.User_UserAgent(userAgent),
|
||||
},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateUserProjectLimits is a method to update the user's usage limits for new projects.
|
||||
func (users *users) UpdateUserProjectLimits(ctx context.Context, id uuid.UUID, limits console.UsageLimits) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -125,6 +125,7 @@ require (
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect
|
||||
github.com/zeebo/admission/v3 v3.0.3 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
github.com/zeebo/errs/v2 v2.0.3 // indirect
|
||||
github.com/zeebo/float16 v0.1.0 // indirect
|
||||
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 // indirect
|
||||
github.com/zeebo/mwc v0.0.4 // indirect
|
||||
|
@ -836,6 +836,7 @@ github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC
|
||||
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
||||
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/errs/v2 v2.0.3 h1:WwqAmopgot4ZC+CgIveP+H91Nf78NDEGWjtAXen45Hw=
|
||||
github.com/zeebo/errs/v2 v2.0.3/go.mod h1:OKmvVZt4UqpyJrYFykDKm168ZquJ55pbbIVUICNmLN0=
|
||||
github.com/zeebo/float16 v0.1.0 h1:kRqxv5og6z1emEyz5FpW0/BVHe5VfxEAw6b1ljCZlUc=
|
||||
github.com/zeebo/float16 v0.1.0/go.mod h1:fssGvvXu+XS8MH57cKmyrLB/cqioYeYX/2mXCN3a5wo=
|
||||
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 h1:+cwNE5KJ3pika4HuzmDHkDlK5myo0G9Sv+eO7WWxnUQ=
|
||||
|
@ -185,6 +185,7 @@ require (
|
||||
github.com/zeebo/admission/v3 v3.0.3 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
github.com/zeebo/errs v1.3.0 // indirect
|
||||
github.com/zeebo/errs/v2 v2.0.3 // indirect
|
||||
github.com/zeebo/float16 v0.1.0 // indirect
|
||||
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 // indirect
|
||||
github.com/zeebo/mwc v0.0.4 // indirect
|
||||
|
@ -1203,6 +1203,7 @@ github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC
|
||||
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
||||
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/errs/v2 v2.0.3 h1:WwqAmopgot4ZC+CgIveP+H91Nf78NDEGWjtAXen45Hw=
|
||||
github.com/zeebo/errs/v2 v2.0.3/go.mod h1:OKmvVZt4UqpyJrYFykDKm168ZquJ55pbbIVUICNmLN0=
|
||||
github.com/zeebo/float16 v0.1.0 h1:kRqxv5og6z1emEyz5FpW0/BVHe5VfxEAw6b1ljCZlUc=
|
||||
github.com/zeebo/float16 v0.1.0/go.mod h1:fssGvvXu+XS8MH57cKmyrLB/cqioYeYX/2mXCN3a5wo=
|
||||
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 h1:+cwNE5KJ3pika4HuzmDHkDlK5myo0G9Sv+eO7WWxnUQ=
|
||||
|
Loading…
Reference in New Issue
Block a user