cmd/tools: remove migrate-segment-copies tool
Migration was done. We can remove tool now. https://github.com/storj/storj/issues/5891 Change-Id: I5d56bad1ac680cd77dabfcf271788e100a6a435b
This commit is contained in:
parent
a5cbec7b3b
commit
7f249ab7ca
@ -1,250 +0,0 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/private/cfgstruct"
|
||||
"storj.io/private/dbutil/pgutil"
|
||||
"storj.io/private/process"
|
||||
"storj.io/private/tagsql"
|
||||
"storj.io/storj/satellite/metabase"
|
||||
)
|
||||
|
||||
var mon = monkit.Package()
|
||||
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "migrate-segment-copies",
|
||||
Short: "migrate-segment-copies",
|
||||
}
|
||||
|
||||
runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "run migrate-segment-copies",
|
||||
RunE: run,
|
||||
}
|
||||
|
||||
config Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
|
||||
cfgstruct.Bind(pflag.CommandLine, &config)
|
||||
}
|
||||
|
||||
// Config defines configuration for migration.
|
||||
type Config struct {
|
||||
MetabaseDB string `help:"connection URL for metabaseDB"`
|
||||
BatchSize int `help:"number of entries from segment_copies processed at once" default:"2000"`
|
||||
SegmentCopiesBackup string `help:"cvs file where segment copies entries will be backup"`
|
||||
}
|
||||
|
||||
// VerifyFlags verifies whether the values provided are valid.
|
||||
func (config *Config) VerifyFlags() error {
|
||||
var errlist errs.Group
|
||||
if config.MetabaseDB == "" {
|
||||
errlist.Add(errors.New("flag '--metabasedb' is not set"))
|
||||
}
|
||||
return errlist.Err()
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
if err := config.VerifyFlags(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, _ := process.Ctx(cmd)
|
||||
log := zap.L()
|
||||
return Migrate(ctx, log, config)
|
||||
}
|
||||
|
||||
func main() {
|
||||
process.Exec(rootCmd)
|
||||
}
|
||||
|
||||
// Migrate starts segment copies migration.
|
||||
func Migrate(ctx context.Context, log *zap.Logger, config Config) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
db, err := metabase.Open(ctx, log, config.MetabaseDB, metabase.Config{
|
||||
ApplicationName: "migrate-segment-copies",
|
||||
})
|
||||
if err != nil {
|
||||
return errs.New("unable to connect %q: %w", config.MetabaseDB, err)
|
||||
}
|
||||
defer func() {
|
||||
err = errs.Combine(err, db.Close())
|
||||
}()
|
||||
|
||||
return MigrateSegments(ctx, log, db, config)
|
||||
}
|
||||
|
||||
// MigrateSegments updates segment copies with proper metadata (pieces and placment).
|
||||
func MigrateSegments(ctx context.Context, log *zap.Logger, metabaseDB *metabase.DB, config Config) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
var backupCSV *csv.Writer
|
||||
if config.SegmentCopiesBackup != "" {
|
||||
f, err := os.Create(config.SegmentCopiesBackup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errs.Combine(err, f.Close())
|
||||
}()
|
||||
|
||||
backupCSV = csv.NewWriter(f)
|
||||
|
||||
defer backupCSV.Flush()
|
||||
|
||||
if err := backupCSV.Write([]string{"stream_id", "ancestor_stream_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
db := metabaseDB.UnderlyingTagSQL()
|
||||
|
||||
var streamIDCursor uuid.UUID
|
||||
ancestorStreamIDs := []uuid.UUID{}
|
||||
streamIDs := []uuid.UUID{}
|
||||
processed := 0
|
||||
|
||||
// what we are doing here:
|
||||
// * read batch of entries from segment_copies table
|
||||
// * read ancestors (original) segments metadata from segments table
|
||||
// * update segment copies with missing metadata, one by one
|
||||
// * delete entries from segment_copies table
|
||||
for {
|
||||
log.Info("Processed entries", zap.Int("processed", processed))
|
||||
|
||||
ancestorStreamIDs = ancestorStreamIDs[:0]
|
||||
streamIDs = streamIDs[:0]
|
||||
|
||||
idsMap := map[uuid.UUID][]uuid.UUID{}
|
||||
err := withRows(db.QueryContext(ctx, `
|
||||
SELECT stream_id, ancestor_stream_id FROM segment_copies WHERE stream_id > $1 ORDER BY stream_id LIMIT $2
|
||||
`, streamIDCursor, config.BatchSize))(func(rows tagsql.Rows) error {
|
||||
for rows.Next() {
|
||||
var streamID, ancestorStreamID uuid.UUID
|
||||
err := rows.Scan(&streamID, &ancestorStreamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamIDCursor = streamID
|
||||
ancestorStreamIDs = append(ancestorStreamIDs, ancestorStreamID)
|
||||
streamIDs = append(streamIDs, streamID)
|
||||
|
||||
idsMap[ancestorStreamID] = append(idsMap[ancestorStreamID], streamID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
StreamID uuid.UUID
|
||||
AncestorStreamID uuid.UUID
|
||||
Position int64
|
||||
RemoteAliasPieces []byte
|
||||
RootPieceID []byte
|
||||
RepairedAt *time.Time
|
||||
Placement *int64
|
||||
}
|
||||
|
||||
updates := []Update{}
|
||||
err = withRows(db.QueryContext(ctx, `
|
||||
SELECT stream_id, position, remote_alias_pieces, root_piece_id, repaired_at, placement FROM segments WHERE stream_id = ANY($1::BYTEA[])
|
||||
`, pgutil.UUIDArray(ancestorStreamIDs)))(func(rows tagsql.Rows) error {
|
||||
for rows.Next() {
|
||||
var ancestorStreamID uuid.UUID
|
||||
var position int64
|
||||
var remoteAliasPieces, rootPieceID []byte
|
||||
var repairedAt *time.Time
|
||||
var placement *int64
|
||||
err := rows.Scan(&ancestorStreamID, &position, &remoteAliasPieces, &rootPieceID, &repairedAt, &placement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamIDs, ok := idsMap[ancestorStreamID]
|
||||
if !ok {
|
||||
return errs.New("unable to map ancestor stream id: %s", ancestorStreamID)
|
||||
}
|
||||
|
||||
for _, streamID := range streamIDs {
|
||||
updates = append(updates, Update{
|
||||
StreamID: streamID,
|
||||
AncestorStreamID: ancestorStreamID,
|
||||
Position: position,
|
||||
RemoteAliasPieces: remoteAliasPieces,
|
||||
RootPieceID: rootPieceID,
|
||||
RepairedAt: repairedAt,
|
||||
Placement: placement,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE segments SET
|
||||
remote_alias_pieces = $3,
|
||||
root_piece_id = $4,
|
||||
repaired_at = $5,
|
||||
placement = $6
|
||||
WHERE (stream_id, position) = ($1, $2)
|
||||
`, update.StreamID, update.Position, update.RemoteAliasPieces, update.RootPieceID, update.RepairedAt, update.Placement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if backupCSV != nil {
|
||||
if err := backupCSV.Write([]string{update.StreamID.String(), update.AncestorStreamID.String()}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if backupCSV != nil {
|
||||
backupCSV.Flush()
|
||||
}
|
||||
|
||||
processed += len(streamIDs)
|
||||
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withRows(rows tagsql.Rows, err error) func(func(tagsql.Rows) error) error {
|
||||
return func(callback func(tagsql.Rows) error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := callback(rows)
|
||||
return errs.Combine(rows.Err(), rows.Close(), err)
|
||||
}
|
||||
}
|
@ -1,324 +0,0 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/common/uuid"
|
||||
cmd "storj.io/storj/cmd/tools/migrate-segment-copies"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite/metabase"
|
||||
"storj.io/storj/satellite/metabase/metabasetest"
|
||||
)
|
||||
|
||||
func TestMigrateSingleCopy(t *testing.T) {
|
||||
metabasetest.Run(t, func(ctx *testcontext.Context, t *testing.T, metabaseDB *metabase.DB) {
|
||||
obj := metabasetest.RandObjectStream()
|
||||
|
||||
expectedPieces := metabase.Pieces{
|
||||
{Number: 1, StorageNode: testrand.NodeID()},
|
||||
{Number: 3, StorageNode: testrand.NodeID()},
|
||||
}
|
||||
|
||||
object, _ := metabasetest.CreateTestObject{
|
||||
CreateSegment: func(object metabase.Object, index int) metabase.Segment {
|
||||
metabasetest.CommitSegment{
|
||||
Opts: metabase.CommitSegment{
|
||||
ObjectStream: obj,
|
||||
Position: metabase.SegmentPosition{Part: 0, Index: uint32(index)},
|
||||
RootPieceID: testrand.PieceID(),
|
||||
|
||||
Pieces: expectedPieces,
|
||||
|
||||
EncryptedKey: []byte{3},
|
||||
EncryptedKeyNonce: []byte{4},
|
||||
EncryptedETag: []byte{5},
|
||||
|
||||
EncryptedSize: 1024,
|
||||
PlainSize: 512,
|
||||
PlainOffset: 0,
|
||||
Redundancy: metabasetest.DefaultRedundancy,
|
||||
Placement: storj.EEA,
|
||||
},
|
||||
}.Check(ctx, t, metabaseDB)
|
||||
|
||||
return metabase.Segment{}
|
||||
},
|
||||
}.Run(ctx, t, metabaseDB, obj, 50)
|
||||
|
||||
copyObject, _, _ := metabasetest.CreateObjectCopy{
|
||||
OriginalObject: object,
|
||||
}.Run(ctx, t, metabaseDB, false)
|
||||
|
||||
segments, err := metabaseDB.TestingAllSegments(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, segment := range segments {
|
||||
if segment.StreamID == copyObject.StreamID {
|
||||
require.Len(t, segment.Pieces, 0)
|
||||
require.Equal(t, storj.EveryCountry, segment.Placement)
|
||||
}
|
||||
}
|
||||
|
||||
require.NotZero(t, numberOfSegmentCopies(t, ctx, metabaseDB))
|
||||
|
||||
err = cmd.MigrateSegments(ctx, zaptest.NewLogger(t), metabaseDB, cmd.Config{
|
||||
BatchSize: 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
segments, err = metabaseDB.TestingAllSegments(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, segment := range segments {
|
||||
require.Equal(t, expectedPieces, segment.Pieces)
|
||||
require.Equal(t, storj.EEA, segment.Placement)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateManyCopies(t *testing.T) {
|
||||
metabasetest.Run(t, func(ctx *testcontext.Context, t *testing.T, metabaseDB *metabase.DB) {
|
||||
obj := metabasetest.RandObjectStream()
|
||||
|
||||
expectedPieces := metabase.Pieces{
|
||||
{Number: 1, StorageNode: testrand.NodeID()},
|
||||
{Number: 3, StorageNode: testrand.NodeID()},
|
||||
}
|
||||
|
||||
object, _ := metabasetest.CreateTestObject{
|
||||
CreateSegment: func(object metabase.Object, index int) metabase.Segment {
|
||||
metabasetest.CommitSegment{
|
||||
Opts: metabase.CommitSegment{
|
||||
ObjectStream: obj,
|
||||
Position: metabase.SegmentPosition{Part: 0, Index: uint32(index)},
|
||||
RootPieceID: testrand.PieceID(),
|
||||
|
||||
Pieces: expectedPieces,
|
||||
|
||||
EncryptedKey: []byte{3},
|
||||
EncryptedKeyNonce: []byte{4},
|
||||
EncryptedETag: []byte{5},
|
||||
|
||||
EncryptedSize: 1024,
|
||||
PlainSize: 512,
|
||||
PlainOffset: 0,
|
||||
Redundancy: metabasetest.DefaultRedundancy,
|
||||
Placement: storj.EEA,
|
||||
},
|
||||
}.Check(ctx, t, metabaseDB)
|
||||
|
||||
return metabase.Segment{}
|
||||
},
|
||||
}.Run(ctx, t, metabaseDB, obj, 20)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
copyObject, _, _ := metabasetest.CreateObjectCopy{
|
||||
OriginalObject: object,
|
||||
}.Run(ctx, t, metabaseDB, false)
|
||||
|
||||
segments, err := metabaseDB.TestingAllSegments(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, segment := range segments {
|
||||
if segment.StreamID == copyObject.StreamID {
|
||||
require.Len(t, segment.Pieces, 0)
|
||||
require.Equal(t, storj.EveryCountry, segment.Placement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require.NotZero(t, numberOfSegmentCopies(t, ctx, metabaseDB))
|
||||
|
||||
err := cmd.MigrateSegments(ctx, zaptest.NewLogger(t), metabaseDB, cmd.Config{
|
||||
BatchSize: 7,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
segments, err := metabaseDB.TestingAllSegments(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, segment := range segments {
|
||||
require.Equal(t, expectedPieces, segment.Pieces)
|
||||
require.Equal(t, storj.EEA, segment.Placement)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateDifferentSegment(t *testing.T) {
|
||||
metabasetest.Run(t, func(ctx *testcontext.Context, t *testing.T, metabaseDB *metabase.DB) {
|
||||
type Segment struct {
|
||||
StreamID uuid.UUID
|
||||
Position int64
|
||||
}
|
||||
|
||||
expectedResults := map[Segment]metabase.Pieces{}
|
||||
createData := func(numberOfObjecsts int, pieces metabase.Pieces) {
|
||||
for i := 0; i < numberOfObjecsts; i++ {
|
||||
numberOfSegments := 3
|
||||
obj := metabasetest.RandObjectStream()
|
||||
object, _ := metabasetest.CreateTestObject{
|
||||
CreateSegment: func(object metabase.Object, index int) metabase.Segment {
|
||||
metabasetest.CommitSegment{
|
||||
Opts: metabase.CommitSegment{
|
||||
ObjectStream: obj,
|
||||
Position: metabase.SegmentPosition{Part: 0, Index: uint32(index)},
|
||||
RootPieceID: testrand.PieceID(),
|
||||
|
||||
Pieces: pieces,
|
||||
|
||||
EncryptedKey: []byte{3},
|
||||
EncryptedKeyNonce: []byte{4},
|
||||
EncryptedETag: []byte{5},
|
||||
|
||||
EncryptedSize: 1024,
|
||||
PlainSize: 512,
|
||||
PlainOffset: 0,
|
||||
Redundancy: metabasetest.DefaultRedundancy,
|
||||
Placement: storj.EEA,
|
||||
},
|
||||
}.Check(ctx, t, metabaseDB)
|
||||
|
||||
return metabase.Segment{}
|
||||
},
|
||||
}.Run(ctx, t, metabaseDB, obj, 3)
|
||||
for n := 0; n < numberOfSegments; n++ {
|
||||
expectedResults[Segment{
|
||||
StreamID: object.StreamID,
|
||||
Position: int64(n),
|
||||
}] = pieces
|
||||
}
|
||||
|
||||
copyObject, _, _ := metabasetest.CreateObjectCopy{
|
||||
OriginalObject: object,
|
||||
}.Run(ctx, t, metabaseDB, false)
|
||||
|
||||
for n := 0; n < numberOfSegments; n++ {
|
||||
expectedResults[Segment{
|
||||
StreamID: copyObject.StreamID,
|
||||
Position: int64(n),
|
||||
}] = pieces
|
||||
|
||||
segments, err := metabaseDB.TestingAllSegments(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, segment := range segments {
|
||||
if segment.StreamID == copyObject.StreamID {
|
||||
require.Len(t, segment.Pieces, 0)
|
||||
require.Equal(t, storj.EveryCountry, segment.Placement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expectedPieces := metabase.Pieces{
|
||||
{Number: 1, StorageNode: testrand.NodeID()},
|
||||
{Number: 3, StorageNode: testrand.NodeID()},
|
||||
}
|
||||
createData(5, expectedPieces)
|
||||
|
||||
expectedPieces = metabase.Pieces{
|
||||
{Number: 2, StorageNode: testrand.NodeID()},
|
||||
{Number: 4, StorageNode: testrand.NodeID()},
|
||||
}
|
||||
createData(5, expectedPieces)
|
||||
|
||||
require.NotZero(t, numberOfSegmentCopies(t, ctx, metabaseDB))
|
||||
|
||||
err := cmd.MigrateSegments(ctx, zaptest.NewLogger(t), metabaseDB, cmd.Config{
|
||||
BatchSize: 7,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
segments, err := metabaseDB.TestingAllSegments(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(expectedResults), len(segments))
|
||||
for _, segment := range segments {
|
||||
pieces := expectedResults[Segment{
|
||||
StreamID: segment.StreamID,
|
||||
Position: int64(segment.Position.Encode()),
|
||||
}]
|
||||
require.Equal(t, pieces, segment.Pieces)
|
||||
require.Equal(t, storj.EEA, segment.Placement)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func numberOfSegmentCopies(t *testing.T, ctx *testcontext.Context, metabaseDB *metabase.DB) int {
|
||||
var count int
|
||||
err := metabaseDB.UnderlyingTagSQL().QueryRow(ctx, "SELECT count(1) FROM segment_copies").Scan(&count)
|
||||
require.NoError(t, err)
|
||||
return count
|
||||
}
|
||||
|
||||
func TestMigrateEndToEnd(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 4, UplinkCount: 1,
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
expectedData := testrand.Bytes(10 * memory.KiB)
|
||||
err := planet.Uplinks[0].Upload(ctx, planet.Satellites[0], "test", "object", expectedData)
|
||||
require.NoError(t, err)
|
||||
|
||||
project, err := planet.Uplinks[0].OpenProject(ctx, planet.Satellites[0])
|
||||
require.NoError(t, err)
|
||||
defer ctx.Check(project.Close)
|
||||
|
||||
_, err = project.CopyObject(ctx, "test", "object", "test", "object-copy", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := planet.Uplinks[0].Download(ctx, planet.Satellites[0], "test", "object-copy")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedData, data)
|
||||
|
||||
err = cmd.MigrateSegments(ctx, zaptest.NewLogger(t), planet.Satellites[0].Metabase.DB, cmd.Config{
|
||||
BatchSize: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err = planet.Uplinks[0].Download(ctx, planet.Satellites[0], "test", "object-copy")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedData, data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateBackupCSV(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 4, UplinkCount: 1,
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
expectedData := testrand.Bytes(10 * memory.KiB)
|
||||
err := planet.Uplinks[0].Upload(ctx, planet.Satellites[0], "test", "object", expectedData)
|
||||
require.NoError(t, err)
|
||||
|
||||
project, err := planet.Uplinks[0].OpenProject(ctx, planet.Satellites[0])
|
||||
require.NoError(t, err)
|
||||
defer ctx.Check(project.Close)
|
||||
|
||||
_, err = project.CopyObject(ctx, "test", "object", "test", "object-copy", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := planet.Uplinks[0].Download(ctx, planet.Satellites[0], "test", "object-copy")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedData, data)
|
||||
|
||||
backupFile := ctx.File("backupcsv")
|
||||
err = cmd.MigrateSegments(ctx, zaptest.NewLogger(t), planet.Satellites[0].Metabase.DB, cmd.Config{
|
||||
BatchSize: 1,
|
||||
SegmentCopiesBackup: backupFile,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err = planet.Uplinks[0].Download(ctx, planet.Satellites[0], "test", "object-copy")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedData, data)
|
||||
|
||||
fileByes, err := os.ReadFile(backupFile)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, fileByes)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user