Add API, CLI and web UI integration for objects
This change adds the API endpoints, the CLI commands and the web UI elements needed to manage objects in GARMs internal storage. This storage system is meant to be used to distribute the garm-agent and as a single source of truth for provider binaries, when we will add the ability for GARM to scale out. Potentially, we can also use this in air gapped systems to distribute the runner binaries for forges that don't have their own internal storage system (like GHES). Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
parent
f66f95baff
commit
6c46cf9be1
138 changed files with 7911 additions and 267 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -18,17 +19,18 @@ import (
|
|||
"github.com/cloudbase/garm/util"
|
||||
)
|
||||
|
||||
func (s *sqlDatabase) CreateFileObject(ctx context.Context, name string, size int64, tags []string, reader io.Reader) (fileObjParam params.FileObject, err error) {
|
||||
// func (s *sqlDatabase) CreateFileObject(ctx context.Context, name string, size int64, tags []string, reader io.Reader) (fileObjParam params.FileObject, err error) {
|
||||
func (s *sqlDatabase) CreateFileObject(ctx context.Context, param params.CreateFileObjectParams, reader io.Reader) (fileObjParam params.FileObject, err error) {
|
||||
// Read first 8KB for type detection
|
||||
buffer := make([]byte, 8192)
|
||||
n, _ := io.ReadFull(reader, buffer)
|
||||
fileType := util.DetectFileType(buffer[:n])
|
||||
// Create document with pre-allocated blob
|
||||
|
||||
fileObj := FileObject{
|
||||
Name: name,
|
||||
FileType: fileType,
|
||||
Size: size,
|
||||
Content: make([]byte, size),
|
||||
Name: param.Name,
|
||||
Description: param.Description,
|
||||
FileType: fileType,
|
||||
Size: param.Size,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
|
@ -37,10 +39,19 @@ func (s *sqlDatabase) CreateFileObject(ctx context.Context, name string, size in
|
|||
}
|
||||
}()
|
||||
|
||||
// Create the file first, without any space allocated for the blob.
|
||||
if err := s.conn.Create(&fileObj).Error; err != nil {
|
||||
return params.FileObject{}, fmt.Errorf("failed to create file object: %w", err)
|
||||
}
|
||||
|
||||
// allocate space for the blob using the zeroblob() function. This will allow us to avoid
|
||||
// having to allocate potentially huge byte arrays in memory and writing that huge blob to
|
||||
// disk.
|
||||
query := fmt.Sprintf(`UPDATE %q SET content = zeroblob(?) WHERE id = ?`, fileObj.TableName())
|
||||
if err := s.conn.Exec(query, param.Size, fileObj.ID).Error; err != nil {
|
||||
return params.FileObject{}, fmt.Errorf("failed to allocate disk space: %w", err)
|
||||
}
|
||||
|
||||
// Stream file to blob and compute SHA256
|
||||
conn, err := s.sqlDB.Conn(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -54,7 +65,7 @@ func (s *sqlDatabase) CreateFileObject(ctx context.Context, name string, size in
|
|||
|
||||
blob, err := sqliteConn.Blob("main", fileObj.TableName(), "content", int64(fileObj.ID), 1)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to open blob: %w", err)
|
||||
}
|
||||
defer blob.Close()
|
||||
|
||||
|
|
@ -63,14 +74,14 @@ func (s *sqlDatabase) CreateFileObject(ctx context.Context, name string, size in
|
|||
|
||||
// Write the buffered data first
|
||||
if _, err := blob.Write(buffer[:n]); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to write blob initial buffer: %w", err)
|
||||
}
|
||||
hasher.Write(buffer[:n])
|
||||
|
||||
// Stream the rest with hash computation
|
||||
_, err = io.Copy(io.MultiWriter(blob, hasher), reader)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to write blob: %w", err)
|
||||
}
|
||||
|
||||
// Get final hash
|
||||
|
|
@ -87,7 +98,7 @@ func (s *sqlDatabase) CreateFileObject(ctx context.Context, name string, size in
|
|||
}
|
||||
|
||||
// Create tag entries
|
||||
for _, tag := range tags {
|
||||
for _, tag := range param.Tags {
|
||||
fileObjTag := FileObjectTag{
|
||||
FileObjectID: fileObj.ID,
|
||||
Tag: tag,
|
||||
|
|
@ -129,6 +140,10 @@ func (s *sqlDatabase) UpdateFileObject(_ context.Context, objID uint, param para
|
|||
fileObj.Name = *param.Name
|
||||
}
|
||||
|
||||
if param.Description != nil {
|
||||
fileObj.Description = *param.Description
|
||||
}
|
||||
|
||||
// Update tags if provided
|
||||
if param.Tags != nil {
|
||||
// Delete existing tags
|
||||
|
|
@ -239,9 +254,20 @@ func (s *sqlDatabase) SearchFileObjectByTags(_ context.Context, tags []string, p
|
|||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
queryPageSize := math.MaxInt
|
||||
if pageSize <= math.MaxInt {
|
||||
queryPageSize = int(pageSize)
|
||||
}
|
||||
|
||||
var queryOffset int
|
||||
if offset <= math.MaxInt {
|
||||
queryOffset = int(offset)
|
||||
} else {
|
||||
return params.FileObjectPaginatedResponse{}, fmt.Errorf("offset excedes max int size: %d", math.MaxInt)
|
||||
}
|
||||
if err := query.
|
||||
Limit(int(pageSize)).
|
||||
Offset(int(offset)).
|
||||
Limit(queryPageSize).
|
||||
Offset(queryOffset).
|
||||
Order("created_at DESC").
|
||||
Omit("content").
|
||||
Find(&fileObjectRes).Error; err != nil {
|
||||
|
|
@ -343,10 +369,23 @@ func (s *sqlDatabase) ListFileObjects(_ context.Context, page, pageSize uint64)
|
|||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
queryPageSize := math.MaxInt
|
||||
if pageSize <= math.MaxInt {
|
||||
queryPageSize = int(pageSize)
|
||||
}
|
||||
|
||||
var queryOffset int
|
||||
if offset <= math.MaxInt {
|
||||
queryOffset = int(offset)
|
||||
} else {
|
||||
return params.FileObjectPaginatedResponse{}, fmt.Errorf("offset excedes max int size: %d", math.MaxInt)
|
||||
}
|
||||
|
||||
var fileObjs []FileObject
|
||||
if err := s.conn.Preload("TagsList").Omit("content").
|
||||
Limit(int(pageSize)).
|
||||
Offset(int(offset)).
|
||||
Limit(queryPageSize).
|
||||
Offset(queryOffset).
|
||||
Order("created_at DESC").
|
||||
Find(&fileObjs).Error; err != nil {
|
||||
return params.FileObjectPaginatedResponse{}, fmt.Errorf("failed to list file objects: %w", err)
|
||||
|
|
@ -384,13 +423,14 @@ func (s *sqlDatabase) sqlFileObjectToCommonParams(obj FileObject) params.FileObj
|
|||
tags[idx] = val.Tag
|
||||
}
|
||||
return params.FileObject{
|
||||
ID: obj.ID,
|
||||
CreatedAt: obj.CreatedAt,
|
||||
UpdatedAt: obj.UpdatedAt,
|
||||
Name: obj.Name,
|
||||
Size: obj.Size,
|
||||
FileType: obj.FileType,
|
||||
SHA256: obj.SHA256,
|
||||
Tags: tags,
|
||||
ID: obj.ID,
|
||||
CreatedAt: obj.CreatedAt,
|
||||
UpdatedAt: obj.UpdatedAt,
|
||||
Name: obj.Name,
|
||||
Size: obj.Size,
|
||||
FileType: obj.FileType,
|
||||
SHA256: obj.SHA256,
|
||||
Description: obj.Description,
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,12 @@ func (s *FileStoreTestSuite) SetupTest() {
|
|||
|
||||
// File 1: Small text file with tags
|
||||
content1 := []byte("Hello, World! This is test file 1.")
|
||||
fileObj1, err := s.Store.CreateFileObject(s.ctx, "test-file-1.txt", int64(len(content1)), []string{"test", "text"}, bytes.NewReader(content1))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "test-file-1.txt",
|
||||
Size: int64(len(content1)),
|
||||
Tags: []string{"test", "text"},
|
||||
}
|
||||
fileObj1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
|
||||
if err != nil {
|
||||
s.FailNow(fmt.Sprintf("failed to create test file 1: %s", err))
|
||||
}
|
||||
|
|
@ -75,7 +80,12 @@ func (s *FileStoreTestSuite) SetupTest() {
|
|||
|
||||
// File 2: Binary-like content with different tags
|
||||
content2 := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00} // PNG header-like
|
||||
fileObj2, err := s.Store.CreateFileObject(s.ctx, "test-image.png", int64(len(content2)), []string{"image", "binary"}, bytes.NewReader(content2))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "test-image.png",
|
||||
Size: int64(len(content2)),
|
||||
Tags: []string{"image", "binary"},
|
||||
}
|
||||
fileObj2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
|
||||
if err != nil {
|
||||
s.FailNow(fmt.Sprintf("failed to create test file 2: %s", err))
|
||||
}
|
||||
|
|
@ -83,7 +93,12 @@ func (s *FileStoreTestSuite) SetupTest() {
|
|||
|
||||
// File 3: No tags
|
||||
content3 := []byte("File without tags.")
|
||||
fileObj3, err := s.Store.CreateFileObject(s.ctx, "no-tags.txt", int64(len(content3)), []string{}, bytes.NewReader(content3))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "no-tags.txt",
|
||||
Size: int64(len(content3)),
|
||||
Tags: []string{},
|
||||
}
|
||||
fileObj3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
|
||||
if err != nil {
|
||||
s.FailNow(fmt.Sprintf("failed to create test file 3: %s", err))
|
||||
}
|
||||
|
|
@ -98,7 +113,12 @@ func (s *FileStoreTestSuite) TestCreateFileObject() {
|
|||
content := []byte("New test file content")
|
||||
tags := []string{"new", "test"}
|
||||
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, "new-file.txt", int64(len(content)), tags, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "new-file.txt",
|
||||
Size: int64(len(content)),
|
||||
Tags: tags,
|
||||
}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotZero(fileObj.ID)
|
||||
s.Require().Equal("new-file.txt", fileObj.Name)
|
||||
|
|
@ -115,7 +135,12 @@ func (s *FileStoreTestSuite) TestCreateFileObject() {
|
|||
|
||||
func (s *FileStoreTestSuite) TestCreateFileObjectEmpty() {
|
||||
content := []byte{}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, "empty-file.txt", 0, []string{}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "empty-file.txt",
|
||||
Size: 0,
|
||||
Tags: []string{},
|
||||
}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotZero(fileObj.ID)
|
||||
s.Require().Equal("empty-file.txt", fileObj.Name)
|
||||
|
|
@ -141,7 +166,12 @@ func (s *FileStoreTestSuite) TestGetFileObjectNotFound() {
|
|||
func (s *FileStoreTestSuite) TestOpenFileObjectContent() {
|
||||
// Create a file with known content
|
||||
content := []byte("Test content for reading")
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, "read-test.txt", int64(len(content)), []string{"read"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "read-test.txt",
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"read"},
|
||||
}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Open and read the content
|
||||
|
|
@ -182,7 +212,12 @@ func (s *FileStoreTestSuite) TestListFileObjectsPagination() {
|
|||
// Create more files to test pagination
|
||||
for i := 0; i < 5; i++ {
|
||||
content := []byte(fmt.Sprintf("File %d", i))
|
||||
_, err := s.Store.CreateFileObject(s.ctx, fmt.Sprintf("page-test-%d.txt", i), int64(len(content)), []string{"pagination"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: fmt.Sprintf("page-test-%d.txt", i),
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"pagination"},
|
||||
}
|
||||
_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
|
|
@ -313,7 +348,12 @@ func (s *FileStoreTestSuite) TestUpdateFileObjectEmptyName() {
|
|||
func (s *FileStoreTestSuite) TestDeleteFileObject() {
|
||||
// Create a file to delete
|
||||
content := []byte("To be deleted")
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, "delete-me.txt", int64(len(content)), []string{"delete"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "delete-me.txt",
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"delete"},
|
||||
}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Delete the file
|
||||
|
|
@ -339,8 +379,12 @@ func (s *FileStoreTestSuite) TestCreateFileObjectLargeContent() {
|
|||
for i := range content {
|
||||
content[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, "large-file.bin", int64(size), []string{"large", "binary"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "large-file.bin",
|
||||
Size: int64(size),
|
||||
Tags: []string{"large", "binary"},
|
||||
}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(int64(size), fileObj.Size)
|
||||
|
||||
|
|
@ -357,7 +401,12 @@ func (s *FileStoreTestSuite) TestCreateFileObjectLargeContent() {
|
|||
func (s *FileStoreTestSuite) TestFileObjectImmutableFields() {
|
||||
// Create a file
|
||||
content := []byte("Immutable test content")
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, "immutable-test.txt", int64(len(content)), []string{"original"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "immutable-test.txt",
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"original"},
|
||||
}
|
||||
fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
|
||||
originalSize := fileObj.Size
|
||||
|
|
@ -390,19 +439,39 @@ func (s *FileStoreTestSuite) TestFileObjectImmutableFields() {
|
|||
func (s *FileStoreTestSuite) TestSearchFileObjectByTags() {
|
||||
// Create files with specific tags for searching
|
||||
content1 := []byte("File with tag1 and tag2")
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, "search-file-1.txt", int64(len(content1)), []string{"tag1", "tag2"}, bytes.NewReader(content1))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "search-file-1.txt",
|
||||
Size: int64(len(content1)),
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
}
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content2 := []byte("File with tag1, tag2, and tag3")
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, "search-file-2.txt", int64(len(content2)), []string{"tag1", "tag2", "tag3"}, bytes.NewReader(content2))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "search-file-2.txt",
|
||||
Size: int64(len(content2)),
|
||||
Tags: []string{"tag1", "tag2", "tag3"},
|
||||
}
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content3 := []byte("File with only tag1")
|
||||
file3, err := s.Store.CreateFileObject(s.ctx, "search-file-3.txt", int64(len(content3)), []string{"tag1"}, bytes.NewReader(content3))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "search-file-3.txt",
|
||||
Size: int64(len(content3)),
|
||||
Tags: []string{"tag1"},
|
||||
}
|
||||
file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content4 := []byte("File with tag3 only")
|
||||
_, err = s.Store.CreateFileObject(s.ctx, "search-file-4.txt", int64(len(content4)), []string{"tag3"}, bytes.NewReader(content4))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "search-file-4.txt",
|
||||
Size: int64(len(content4)),
|
||||
Tags: []string{"tag3"},
|
||||
}
|
||||
_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content4))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Search for files with tag1 - should return 3 files
|
||||
|
|
@ -423,15 +492,30 @@ func (s *FileStoreTestSuite) TestSearchFileObjectByTags() {
|
|||
func (s *FileStoreTestSuite) TestSearchFileObjectByTagsMultipleTags() {
|
||||
// Create files with various tag combinations
|
||||
content1 := []byte("File with search1 and search2")
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, "multi-search-1.txt", int64(len(content1)), []string{"search1", "search2"}, bytes.NewReader(content1))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "multi-search-1.txt",
|
||||
Size: int64(len(content1)),
|
||||
Tags: []string{"search1", "search2"},
|
||||
}
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content2 := []byte("File with search1, search2, and search3")
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, "multi-search-2.txt", int64(len(content2)), []string{"search1", "search2", "search3"}, bytes.NewReader(content2))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "multi-search-2.txt",
|
||||
Size: int64(len(content2)),
|
||||
Tags: []string{"search1", "search2", "search3"},
|
||||
}
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content3 := []byte("File with only search1")
|
||||
_, err = s.Store.CreateFileObject(s.ctx, "multi-search-3.txt", int64(len(content3)), []string{"search1"}, bytes.NewReader(content3))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "multi-search-3.txt",
|
||||
Size: int64(len(content3)),
|
||||
Tags: []string{"search1"},
|
||||
}
|
||||
_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Search for files with both search1 AND search2 - should return only 2 files
|
||||
|
|
@ -469,7 +553,12 @@ func (s *FileStoreTestSuite) TestSearchFileObjectByTagsPagination() {
|
|||
// Create multiple files with the same tag
|
||||
for i := 0; i < 5; i++ {
|
||||
content := []byte(fmt.Sprintf("Pagination test file %d", i))
|
||||
_, err := s.Store.CreateFileObject(s.ctx, fmt.Sprintf("page-search-%d.txt", i), int64(len(content)), []string{"pagination-test"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: fmt.Sprintf("page-search-%d.txt", i),
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"pagination-test"},
|
||||
}
|
||||
_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
|
|
@ -497,7 +586,12 @@ func (s *FileStoreTestSuite) TestSearchFileObjectByTagsPagination() {
|
|||
func (s *FileStoreTestSuite) TestSearchFileObjectByTagsDefaultPagination() {
|
||||
// Create a file with a unique tag
|
||||
content := []byte("Default pagination test")
|
||||
_, err := s.Store.CreateFileObject(s.ctx, "default-page-search.txt", int64(len(content)), []string{"default-pagination"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "default-page-search.txt",
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"default-pagination"},
|
||||
}
|
||||
_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Test default values (page 0 should become 1, pageSize 0 should become 20)
|
||||
|
|
@ -511,19 +605,39 @@ func (s *FileStoreTestSuite) TestSearchFileObjectByTagsDefaultPagination() {
|
|||
func (s *FileStoreTestSuite) TestSearchFileObjectByTagsAllTagsRequired() {
|
||||
// Test that search requires ALL specified tags (AND logic, not OR)
|
||||
content1 := []byte("Has A and B")
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, "and-test-1.txt", int64(len(content1)), []string{"tagA", "tagB"}, bytes.NewReader(content1))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "and-test-1.txt",
|
||||
Size: int64(len(content1)),
|
||||
Tags: []string{"tagA", "tagB"},
|
||||
}
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content2 := []byte("Has A, B, and C")
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, "and-test-2.txt", int64(len(content2)), []string{"tagA", "tagB", "tagC"}, bytes.NewReader(content2))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "and-test-2.txt",
|
||||
Size: int64(len(content2)),
|
||||
Tags: []string{"tagA", "tagB", "tagC"},
|
||||
}
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content3 := []byte("Has only A")
|
||||
_, err = s.Store.CreateFileObject(s.ctx, "and-test-3.txt", int64(len(content3)), []string{"tagA"}, bytes.NewReader(content3))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "and-test-3.txt",
|
||||
Size: int64(len(content3)),
|
||||
Tags: []string{"tagA"},
|
||||
}
|
||||
_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content4 := []byte("Has only B")
|
||||
_, err = s.Store.CreateFileObject(s.ctx, "and-test-4.txt", int64(len(content4)), []string{"tagB"}, bytes.NewReader(content4))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "and-test-4.txt",
|
||||
Size: int64(len(content4)),
|
||||
Tags: []string{"tagB"},
|
||||
}
|
||||
_, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content4))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Search for files with BOTH tagA AND tagB
|
||||
|
|
@ -546,15 +660,30 @@ func (s *FileStoreTestSuite) TestSearchFileObjectByTagsAllTagsRequired() {
|
|||
func (s *FileStoreTestSuite) TestSearchFileObjectByTagsCaseInsensitive() {
|
||||
// Test case insensitivity of tag search (COLLATE NOCASE)
|
||||
content1 := []byte("File with lowercase tag")
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, "case-test-1.txt", int64(len(content1)), []string{"TestTag"}, bytes.NewReader(content1))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "case-test-1.txt",
|
||||
Size: int64(len(content1)),
|
||||
Tags: []string{"TestTag"},
|
||||
}
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content2 := []byte("File with UPPERCASE tag")
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, "case-test-2.txt", int64(len(content2)), []string{"TESTTAG"}, bytes.NewReader(content2))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "case-test-2.txt",
|
||||
Size: int64(len(content2)),
|
||||
Tags: []string{"TESTTAG"},
|
||||
}
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content3 := []byte("File with MixedCase tag")
|
||||
file3, err := s.Store.CreateFileObject(s.ctx, "case-test-3.txt", int64(len(content3)), []string{"testTAG"}, bytes.NewReader(content3))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "case-test-3.txt",
|
||||
Size: int64(len(content3)),
|
||||
Tags: []string{"testTAG"},
|
||||
}
|
||||
file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Search for lowercase - should return all files (case insensitive)
|
||||
|
|
@ -586,15 +715,30 @@ func (s *FileStoreTestSuite) TestSearchFileObjectByTagsOrderByCreatedAt() {
|
|||
tag := "order-test"
|
||||
|
||||
content1 := []byte("First file")
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, "order-1.txt", int64(len(content1)), []string{tag}, bytes.NewReader(content1))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "order-1.txt",
|
||||
Size: int64(len(content1)),
|
||||
Tags: []string{tag},
|
||||
}
|
||||
file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content2 := []byte("Second file")
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, "order-2.txt", int64(len(content2)), []string{tag}, bytes.NewReader(content2))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "order-2.txt",
|
||||
Size: int64(len(content2)),
|
||||
Tags: []string{tag},
|
||||
}
|
||||
file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2))
|
||||
s.Require().Nil(err)
|
||||
|
||||
content3 := []byte("Third file")
|
||||
file3, err := s.Store.CreateFileObject(s.ctx, "order-3.txt", int64(len(content3)), []string{tag}, bytes.NewReader(content3))
|
||||
param = params.CreateFileObjectParams{
|
||||
Name: "order-3.txt",
|
||||
Size: int64(len(content3)),
|
||||
Tags: []string{tag},
|
||||
}
|
||||
file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3))
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Search and verify order (should be DESC by created_at, so newest first)
|
||||
|
|
@ -629,7 +773,12 @@ func (s *FileStoreTestSuite) TestPaginationFieldsLastPage() {
|
|||
// Create exactly 5 files
|
||||
for i := 0; i < 5; i++ {
|
||||
content := []byte(fmt.Sprintf("Last page test %d", i))
|
||||
_, err := s.Store.CreateFileObject(s.ctx, fmt.Sprintf("last-page-test-%d.txt", i), int64(len(content)), []string{"last-page"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: fmt.Sprintf("last-page-test-%d.txt", i),
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"last-page"},
|
||||
}
|
||||
_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
|
|
@ -649,7 +798,12 @@ func (s *FileStoreTestSuite) TestPaginationFieldsLastPage() {
|
|||
func (s *FileStoreTestSuite) TestPaginationFieldsSinglePage() {
|
||||
// Test when all results fit in a single page
|
||||
content := []byte("Single page test")
|
||||
_, err := s.Store.CreateFileObject(s.ctx, "single-page-test.txt", int64(len(content)), []string{"single-page-unique-tag"}, bytes.NewReader(content))
|
||||
param := params.CreateFileObjectParams{
|
||||
Name: "single-page-test.txt",
|
||||
Size: int64(len(content)),
|
||||
Tags: []string{"single-page-unique-tag"},
|
||||
}
|
||||
_, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content))
|
||||
s.Require().Nil(err)
|
||||
|
||||
result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"single-page-unique-tag"}, 1, 20)
|
||||
|
|
|
|||
|
|
@ -51,20 +51,20 @@ type GithubTestSuite struct {
|
|||
db common.Store
|
||||
}
|
||||
|
||||
func (s *GithubTestSuite) TearDownTest() {
|
||||
watcher.CloseWatcher()
|
||||
}
|
||||
|
||||
func (s *GithubTestSuite) SetupTest() {
|
||||
ctx := context.Background()
|
||||
watcher.InitWatcher(ctx)
|
||||
db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T()))
|
||||
db, err := NewSQLDatabase(ctx, garmTesting.GetTestSqliteDBConfig(s.T()))
|
||||
if err != nil {
|
||||
s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
|
||||
}
|
||||
s.db = db
|
||||
}
|
||||
|
||||
func (s *GithubTestSuite) TearDownTest() {
|
||||
watcher.CloseWatcher()
|
||||
}
|
||||
|
||||
func (s *GithubTestSuite) TestDefaultEndpointGetsCreatedAutomaticallyIfNoOtherEndpointExists() {
|
||||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
endpoint, err := s.db.GetGithubEndpoint(ctx, defaultGithubEndpoint)
|
||||
|
|
@ -953,9 +953,11 @@ func TestCredentialsAndEndpointMigration(t *testing.T) {
|
|||
// Set the config credentials in the cfg. This is what happens in the main function.
|
||||
// of GARM as well.
|
||||
cfg.MigrateCredentials = credentials
|
||||
|
||||
ctx := context.Background()
|
||||
watcher.InitWatcher(ctx)
|
||||
defer watcher.CloseWatcher()
|
||||
|
||||
db, err := NewSQLDatabase(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create db connection: %s", err)
|
||||
|
|
|
|||
|
|
@ -79,13 +79,13 @@ func (s *InstancesTestSuite) SetupTest() {
|
|||
ctx := context.Background()
|
||||
watcher.InitWatcher(ctx)
|
||||
// create testing sqlite database
|
||||
db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T()))
|
||||
db, err := NewSQLDatabase(ctx, garmTesting.GetTestSqliteDBConfig(s.T()))
|
||||
if err != nil {
|
||||
s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
|
||||
}
|
||||
s.Store = db
|
||||
|
||||
adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T())
|
||||
adminCtx := garmTesting.ImpersonateAdminContext(ctx, db, s.T())
|
||||
s.adminCtx = adminCtx
|
||||
|
||||
githubEndpoint := garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T())
|
||||
|
|
|
|||
|
|
@ -462,7 +462,9 @@ type GiteaCredentials struct {
|
|||
type FileObject struct {
|
||||
gorm.Model
|
||||
// Name is the name of the file
|
||||
Name string `gotm:"type:text,index:idx_fo_name"`
|
||||
Name string `gorm:"type:text;index:idx_fo_name"`
|
||||
// Description is a description for the file
|
||||
Description string `gorm:"type:text"`
|
||||
// FileType holds the MIME type or file type description
|
||||
FileType string `gorm:"type:text"`
|
||||
// Size is the file size in bytes
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ func (s *OrgTestSuite) SetupTest() {
|
|||
watcher.InitWatcher(ctx)
|
||||
// create testing sqlite database
|
||||
dbConfig := garmTesting.GetTestSqliteDBConfig(s.T())
|
||||
db, err := NewSQLDatabase(context.Background(), dbConfig)
|
||||
db, err := NewSQLDatabase(ctx, dbConfig)
|
||||
if err != nil {
|
||||
s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
|
||||
}
|
||||
s.Store = db
|
||||
|
||||
adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T())
|
||||
adminCtx := garmTesting.ImpersonateAdminContext(ctx, db, s.T())
|
||||
s.adminCtx = adminCtx
|
||||
s.adminUserID = auth.UserID(adminCtx)
|
||||
s.Require().NotEmpty(s.adminUserID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue