From 17b40f221437582428ccbf1cb0f6000b9a374b11 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 30 Jul 2025 16:28:33 +0800 Subject: [PATCH] Add collection api. --- frontend/src/network/models.ts | 8 ++ frontend/src/network/network.ts | 94 +++++++++++++ main.go | 6 +- server/api/collection.go | 227 ++++++++++++++++++++++++++++++++ server/dao/collection.go | 193 +++++++++++++++++++++++++++ server/dao/image.go | 4 +- server/model/collection.go | 31 +++++ server/service/collection.go | 145 ++++++++++++++++++++ server/service/comment.go | 34 ----- server/service/utils.go | 35 +++++ 10 files changed, 740 insertions(+), 37 deletions(-) create mode 100644 server/api/collection.go create mode 100644 server/dao/collection.go create mode 100644 server/model/collection.go create mode 100644 server/service/collection.go diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index 233b8d2..75486af 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -191,3 +191,11 @@ export interface Activity { comment?: Comment; file?: RFile; } + +export interface Collection { + id: number; + title: string; + article: string; + user: User; + images: Image[]; +} \ No newline at end of file diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 3c78537..0bd8695 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -19,6 +19,7 @@ import { TagWithCount, Activity, CommentWithRef, + Collection, } from "./models.ts"; class Network { @@ -688,6 +689,99 @@ class Network { }), ); } + + async createCollection( + title: string, + article: string, + ): Promise> { + return this._callApi(() => + axios.postForm(`${this.apiBaseUrl}/collection/create`, { + title, + article, + }), + ); + } + + async updateCollection( + id: number, + title: string, + article: string, + ): Promise> { + return this._callApi(() => + axios.postForm(`${this.apiBaseUrl}/collection/update`, { + id, + title, + article, + }), + ); + } + + async deleteCollection(id: number): Promise> { + return this._callApi(() => + axios.postForm(`${this.apiBaseUrl}/collection/delete`, { + id, + }), + ); + } + + async getCollection(id: number): Promise> { + return this._callApi(() => + axios.get(`${this.apiBaseUrl}/collection/${id}`), + ); + } + + async listUserCollections(page: number = 1): Promise> { + return this._callApi(() => + axios.get(`${this.apiBaseUrl}/collection/list`, { + params: { page }, + }), + ); + } + + async listCollectionResources( + collectionId: number, + page: number = 1, + ): Promise> { + return this._callApi(() => + axios.get(`${this.apiBaseUrl}/collection/${collectionId}/resources`, { + params: { page }, + }), + ); + } + + async addResourceToCollection( + collectionId: number, + resourceId: number, + ): Promise> { + return this._callApi(() => + axios.postForm(`${this.apiBaseUrl}/collection/add_resource`, { + collection_id: collectionId, + resource_id: resourceId, + }), + ); + } + + async removeResourceFromCollection( + collectionId: number, + resourceId: number, + ): Promise> { + return this._callApi(() => + axios.postForm(`${this.apiBaseUrl}/collection/remove_resource`, { + collection_id: collectionId, + resource_id: resourceId, + }), + ); + } + + async searchUserCollections( + keyword: string, + ): Promise> { + return this._callApi(() => + axios.get(`${this.apiBaseUrl}/collection/search`, { + params: { keyword }, + }), + ); + } } export const network = new Network(); diff --git a/main.go b/main.go index 1e363b3..29e0321 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ package main import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/logger" "log" "nysoure/server/api" "nysoure/server/middleware" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/logger" ) func main() { @@ -35,6 +36,7 @@ func main() { api.AddCommentRoutes(apiG) api.AddConfigRoutes(apiG) api.AddActivityRoutes(apiG) + api.AddCollectionRoutes(apiG) // 新增 } log.Fatal(app.Listen(":3000")) diff --git a/server/api/collection.go b/server/api/collection.go new file mode 100644 index 0000000..e749f9b --- /dev/null +++ b/server/api/collection.go @@ -0,0 +1,227 @@ +package api + +import ( + "nysoure/server/model" + "nysoure/server/service" + "strconv" + + "github.com/gofiber/fiber/v3" +) + +func handleCreateCollection(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + title := c.FormValue("title") + article := c.FormValue("article") + if title == "" || article == "" { + return model.NewRequestError("Title and article are required") + } + host := c.Hostname() + col, err := service.CreateCollection(uid, title, article, host) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[model.CollectionView]{ + Success: true, + Data: *col, + Message: "Collection created successfully", + }) +} + +func handleUpdateCollection(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + idStr := c.FormValue("id") + title := c.FormValue("title") + article := c.FormValue("article") + if idStr == "" || title == "" || article == "" { + return model.NewRequestError("ID, title and article are required") + } + id, err := strconv.Atoi(idStr) + if err != nil { + return model.NewRequestError("Invalid collection ID") + } + host := c.Hostname() + if err := service.UpdateCollection(uid, uint(id), title, article, host); err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[any]{ + Success: true, + Message: "Collection updated successfully", + }) +} + +func handleDeleteCollection(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + idStr := c.FormValue("id") + if idStr == "" { + return model.NewRequestError("ID is required") + } + id, err := strconv.Atoi(idStr) + if err != nil { + return model.NewRequestError("Invalid collection ID") + } + if err := service.DeleteCollection(uid, uint(id)); err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[any]{ + Success: true, + Message: "Collection deleted successfully", + }) +} + +func handleGetCollection(c fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.Atoi(idStr) + if err != nil { + return model.NewRequestError("Invalid collection ID") + } + col, err := service.GetCollectionByID(uint(id)) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[model.CollectionView]{ + Success: true, + Data: *col, + Message: "Collection retrieved successfully", + }) +} + +func handleListUserCollections(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + pageStr := c.Query("page", "1") + page, err := strconv.Atoi(pageStr) + if err != nil || page < 1 { + page = 1 + } + cols, total, err := service.ListUserCollections(uid, page) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.PageResponse[*model.CollectionView]{ + Success: true, + TotalPages: int(total), + Data: cols, + Message: "Collections retrieved successfully", + }) +} + +func handleListCollectionResources(c fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.Atoi(idStr) + if err != nil { + return model.NewRequestError("Invalid collection ID") + } + pageStr := c.Query("page", "1") + page, err := strconv.Atoi(pageStr) + if err != nil || page < 1 { + page = 1 + } + res, total, err := service.ListCollectionResources(uint(id), page) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.PageResponse[*model.ResourceView]{ + Success: true, + TotalPages: int(total), + Data: res, + Message: "Resources retrieved successfully", + }) +} + +func handleAddResourceToCollection(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + collectionIDStr := c.FormValue("collection_id") + resourceIDStr := c.FormValue("resource_id") + if collectionIDStr == "" || resourceIDStr == "" { + return model.NewRequestError("collection_id and resource_id are required") + } + collectionID, err := strconv.Atoi(collectionIDStr) + if err != nil { + return model.NewRequestError("Invalid collection_id") + } + resourceID, err := strconv.Atoi(resourceIDStr) + if err != nil { + return model.NewRequestError("Invalid resource_id") + } + if err := service.AddResourceToCollection(uid, uint(collectionID), uint(resourceID)); err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[any]{ + Success: true, + Message: "Resource added to collection successfully", + }) +} + +func handleRemoveResourceFromCollection(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + collectionIDStr := c.FormValue("collection_id") + resourceIDStr := c.FormValue("resource_id") + if collectionIDStr == "" || resourceIDStr == "" { + return model.NewRequestError("collection_id and resource_id are required") + } + collectionID, err := strconv.Atoi(collectionIDStr) + if err != nil { + return model.NewRequestError("Invalid collection_id") + } + resourceID, err := strconv.Atoi(resourceIDStr) + if err != nil { + return model.NewRequestError("Invalid resource_id") + } + if err := service.RemoveResourceFromCollection(uid, uint(collectionID), uint(resourceID)); err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[any]{ + Success: true, + Message: "Resource removed from collection successfully", + }) +} + +func handleSearchUserCollections(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + keyword := c.Query("keyword", "") + if keyword == "" { + return model.NewRequestError("keyword is required") + } + cols, err := service.SearchUserCollections(uid, keyword) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[[]*model.CollectionView]{ + Success: true, + Data: cols, + Message: "Collections found successfully", + }) +} + +func AddCollectionRoutes(r fiber.Router) { + cg := r.Group("collection") + cg.Post("/create", handleCreateCollection) + cg.Post("/update", handleUpdateCollection) + cg.Post("/delete", handleDeleteCollection) + cg.Get("/:id", handleGetCollection) + cg.Get("/list", handleListUserCollections) + cg.Get("/:id/resources", handleListCollectionResources) + cg.Post("/add_resource", handleAddResourceToCollection) + cg.Post("/remove_resource", handleRemoveResourceFromCollection) + cg.Get("/search", handleSearchUserCollections) +} diff --git a/server/dao/collection.go b/server/dao/collection.go new file mode 100644 index 0000000..9d55ebf --- /dev/null +++ b/server/dao/collection.go @@ -0,0 +1,193 @@ +package dao + +import ( + "nysoure/server/model" + + "gorm.io/gorm" +) + +func CreateCollection(uid uint, title string, article string, images []uint) (model.Collection, error) { + var collection model.Collection + err := db.Transaction(func(tx *gorm.DB) error { + collection = model.Collection{ + UserID: uid, + Title: title, + Article: article, + } + + if err := tx.Create(collection).Error; err != nil { + return err + } + + if err := tx.Model(collection).Association("Images").Replace(images); err != nil { + return err + } + + return nil + }) + + if err != nil { + return model.Collection{}, err + } + return collection, nil +} + +func UpdateCollection(id uint, title string, article string, images []uint) error { + return db.Transaction(func(tx *gorm.DB) error { + collection := &model.Collection{ + Title: title, + Article: article, + } + + if err := tx.Model(collection).Where("id = ?", id).Updates(collection).Error; err != nil { + return err + } + + if err := tx.Model(collection).Association("Images").Replace(images); err != nil { + return err + } + + return nil + }) +} + +func DeleteCollection(id uint) error { + return db.Transaction(func(tx *gorm.DB) error { + collection := &model.Collection{} + + if err := tx.Where("id = ?", id).First(collection).Error; err != nil { + return err + } + + if err := tx.Model(collection).Association("Images").Clear(); err != nil { + return err + } + + if err := tx.Model(collection).Association("Resources").Clear(); err != nil { + return err + } + + if err := tx.Delete(collection).Error; err != nil { + return err + } + + return nil + }) +} + +func AddResourceToCollection(collectionID uint, resourceID uint) error { + return db.Transaction(func(tx *gorm.DB) error { + collection := &model.Collection{} + + if err := tx.Where("id = ?", collectionID).First(collection).Error; err != nil { + return err + } + + if err := tx.Model(collection).Association("Resources").Append(&model.Resource{ + Model: gorm.Model{ + ID: resourceID, + }, + }); err != nil { + return err + } + + return nil + }) +} + +func RemoveResourceFromCollection(collectionID uint, resourceID uint) error { + return db.Transaction(func(tx *gorm.DB) error { + collection := &model.Collection{} + + if err := tx.Where("id = ?", collectionID).First(collection).Error; err != nil { + return err + } + + if err := tx.Model(collection).Association("Resources").Delete(&model.Resource{ + Model: gorm.Model{ + ID: resourceID, + }, + }); err != nil { + return err + } + + return nil + }) +} + +func GetCollectionByID(id uint) (*model.Collection, error) { + collection := &model.Collection{} + if err := db.Preload("Images").Preload("Resources").Where("id = ?", id).First(collection).Error; err != nil { + return nil, err + } + return collection, nil +} + +func ListUserCollections(uid uint, page int, pageSize int) ([]*model.Collection, int64, error) { + var collections []*model.Collection + var total int64 + + if err := db.Model(&model.Collection{}).Where("user_id = ?", uid).Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := db. + Model(&model.Collection{}). + Preload("Images"). + Preload("Resources"). + Where("user_id = ?", uid). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&collections).Error; err != nil { + return nil, 0, err + } + + totalPages := (total + int64(pageSize) - 1) / int64(pageSize) + + return collections, totalPages, nil +} + +func ListCollectionResources(collectionID uint, page int, pageSize int) ([]*model.Resource, int64, error) { + var resources []*model.Resource + var total int64 + + if err := db.Raw(` + select count(*) from collection_resources + where collection_id = ?`, collectionID).Scan(&total).Error; err != nil { + return nil, 0, err + } + + if err := db. + Model(&model.Resource{}). + Preload("User"). + Preload("Images"). + Preload("Tags"). + Joins("JOIN collection_resources ON collection_resources.resource_id = resources.id"). + Where("collection_resources.collection_id = ?", collectionID). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&resources).Error; err != nil { + return nil, 0, err + } + + totalPages := (total + int64(pageSize) - 1) / int64(pageSize) + + return resources, totalPages, nil +} + +// SearchUserCollections searches for collections by user ID and keyword limited to 10 results. +func SearchUserCollections(uid uint, keyword string) ([]*model.Collection, error) { + var collections []*model.Collection + + if err := db. + Model(&model.Collection{}). + Preload("Images"). + Preload("Resources"). + Where("user_id = ? AND title LIKE ?", uid, "%"+keyword+"%"). + Limit(10). + Find(&collections).Error; err != nil { + return nil, err + } + + return collections, nil +} diff --git a/server/dao/image.go b/server/dao/image.go index 3a7e232..5882654 100644 --- a/server/dao/image.go +++ b/server/dao/image.go @@ -2,9 +2,10 @@ package dao import ( "errors" - "gorm.io/gorm" "nysoure/server/model" "time" + + "gorm.io/gorm" ) func CreateImage(name string, width, height int) (model.Image, error) { @@ -45,6 +46,7 @@ func GetUnusedImages() ([]model.Image, error) { if err := db. Where("NOT EXISTS (SELECT 1 FROM resource_images WHERE image_id = images.id)"). Where("NOT EXISTS (SELECT 1 FROM comment_images WHERE image_id = images.id)"). + Where("NOT EXISTS (SELECT 1 FROM collection_images WHERE image_id = images.id)"). Where("created_at < ?", oneDayAgo). Find(&images).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/server/model/collection.go b/server/model/collection.go new file mode 100644 index 0000000..549c5c6 --- /dev/null +++ b/server/model/collection.go @@ -0,0 +1,31 @@ +package model + +import "gorm.io/gorm" + +type Collection struct { + gorm.Model + Title string `gorm:"not null"` + Article string `gorm:"not null"` + UserID uint `gorm:"not null"` + User User `gorm:"foreignKey:UserID;references:ID"` + Images []Image `gorm:"many2many:collection_images;"` + Resources []Resource `gorm:"many2many:collection_resources;"` +} + +type CollectionView struct { + ID uint `json:"id"` + Title string `json:"title"` + Article string `json:"article"` + User UserView `json:"user"` + Images []Image `json:"images"` +} + +func (c Collection) ToView() *CollectionView { + return &CollectionView{ + ID: c.ID, + Title: c.Title, + Article: c.Article, + User: c.User.ToView(), + Images: c.Images, + } +} diff --git a/server/service/collection.go b/server/service/collection.go new file mode 100644 index 0000000..391ce49 --- /dev/null +++ b/server/service/collection.go @@ -0,0 +1,145 @@ +package service + +import ( + "errors" + "nysoure/server/dao" + "nysoure/server/model" +) + +// Create a new collection. +func CreateCollection(uid uint, title, article string, host string) (*model.CollectionView, error) { + if uid == 0 || title == "" || article == "" { + return nil, errors.New("invalid parameters") + } + c, err := dao.CreateCollection(uid, title, article, findImagesInContent(article, host)) + if err != nil { + return nil, err + } + view := c.ToView() + return view, nil +} + +// Update an existing collection with user validation. +func UpdateCollection(uid, id uint, title, article string, host string) error { + if uid == 0 || id == 0 || title == "" || article == "" { + return errors.New("invalid parameters") + } + collection, err := dao.GetCollectionByID(id) + if err != nil { + return err + } + if collection.UserID != uid { + return errors.New("user does not have permission to update this collection") + } + return dao.UpdateCollection(id, title, article, findImagesInContent(article, host)) +} + +// Delete a collection by ID. +func DeleteCollection(uint, id uint) error { + user, err := dao.GetUserByID(id) + if err != nil { + return err + } + + collection, err := dao.GetCollectionByID(id) + if err != nil { + return err + } + + if user.ID != collection.UserID && !user.IsAdmin { + return errors.New("user does not have permission to delete this collection") + } + + return dao.DeleteCollection(id) +} + +// Add a resource to a collection with user validation. +func AddResourceToCollection(uid, collectionID, resourceID uint) error { + if uid == 0 || collectionID == 0 || resourceID == 0 { + return errors.New("invalid parameters") + } + collection, err := dao.GetCollectionByID(collectionID) + if err != nil { + return err + } + if collection.UserID != uid { + return errors.New("user does not have permission to modify this collection") + } + return dao.AddResourceToCollection(collectionID, resourceID) +} + +// Remove a resource from a collection with user validation. +func RemoveResourceFromCollection(uid, collectionID, resourceID uint) error { + if uid == 0 || collectionID == 0 || resourceID == 0 { + return errors.New("invalid parameters") + } + collection, err := dao.GetCollectionByID(collectionID) + if err != nil { + return err + } + if collection.UserID != uid { + return errors.New("user does not have permission to modify this collection") + } + return dao.RemoveResourceFromCollection(collectionID, resourceID) +} + +// Get a collection by ID. +func GetCollectionByID(id uint) (*model.CollectionView, error) { + if id == 0 { + return nil, errors.New("invalid collection id") + } + c, err := dao.GetCollectionByID(id) + if err != nil { + return nil, err + } + return c.ToView(), nil +} + +// List collections of a user with pagination. +func ListUserCollections(uid uint, page int) ([]*model.CollectionView, int64, error) { + if uid == 0 || page < 1 { + return nil, 0, errors.New("invalid parameters") + } + collections, total, err := dao.ListUserCollections(uid, page, pageSize) + if err != nil { + return nil, 0, err + } + var views []*model.CollectionView + for _, c := range collections { + views = append(views, c.ToView()) + } + return views, total, nil +} + +// List resources in a collection with pagination. +func ListCollectionResources(collectionID uint, page int) ([]*model.ResourceView, int64, error) { + if collectionID == 0 || page < 1 { + return nil, 0, errors.New("invalid parameters") + } + resources, total, err := dao.ListCollectionResources(collectionID, page, pageSize) + if err != nil { + return nil, 0, err + } + var views []*model.ResourceView + for _, r := range resources { + v := r.ToView() + views = append(views, &v) + } + return views, total, nil +} + +// Search user collections by keyword, limited to 10 results. +func SearchUserCollections(uid uint, keyword string) ([]*model.CollectionView, error) { + if uid == 0 || keyword == "" { + return nil, errors.New("invalid parameters") + } + collections, err := dao.SearchUserCollections(uid, keyword) + if err != nil { + return nil, err + } + var views []*model.CollectionView + for _, c := range collections { + views = append(views, c.ToView()) + } + return views, nil +} diff --git a/server/service/comment.go b/server/service/comment.go index bef505c..994465e 100644 --- a/server/service/comment.go +++ b/server/service/comment.go @@ -4,7 +4,6 @@ import ( "nysoure/server/dao" "nysoure/server/model" "regexp" - "strconv" "strings" "github.com/gofiber/fiber/v3/log" @@ -22,39 +21,6 @@ type CommentRequest struct { // Images []uint `json:"images"` // Unrequired after new design } -func findImagesInContent(content string, host string) []uint { - // Handle both absolute and relative URLs - absolutePattern := `!\[.*?\]\((?:https?://` + host + `)?/api/image/(\d+)(?:\s+["'].*?["'])?\)` - relativePattern := `!\[.*?\]\(/api/image/(\d+)(?:\s+["'].*?["'])?\)` - - // Combine patterns and compile regex - patterns := []string{absolutePattern, relativePattern} - - // Store unique image IDs to avoid duplicates - imageIDs := make(map[uint]struct{}) - - for _, pattern := range patterns { - re := regexp.MustCompile(pattern) - matches := re.FindAllStringSubmatch(content, -1) - - for _, match := range matches { - if len(match) >= 2 { - if id, err := strconv.ParseUint(match[1], 10, 32); err == nil { - imageIDs[uint(id)] = struct{}{} - } - } - } - } - - // Convert map keys to slice - result := make([]uint, 0, len(imageIDs)) - for id := range imageIDs { - result = append(result, id) - } - - return result -} - func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType model.CommentType, host string) (*model.CommentView, error) { if len(req.Content) == 0 { return nil, model.NewRequestError("Content cannot be empty") diff --git a/server/service/utils.go b/server/service/utils.go index 483193f..1fafe47 100644 --- a/server/service/utils.go +++ b/server/service/utils.go @@ -6,6 +6,8 @@ import ( "net/http" "nysoure/server/config" "nysoure/server/dao" + "regexp" + "strconv" ) func checkUserCanUpload(uid uint) (bool, error) { @@ -58,3 +60,36 @@ func verifyCfToken(cfToken string) (bool, error) { return false, nil } } + +func findImagesInContent(content string, host string) []uint { + // Handle both absolute and relative URLs + absolutePattern := `!\[.*?\]\((?:https?://` + host + `)?/api/image/(\d+)(?:\s+["'].*?["'])?\)` + relativePattern := `!\[.*?\]\(/api/image/(\d+)(?:\s+["'].*?["'])?\)` + + // Combine patterns and compile regex + patterns := []string{absolutePattern, relativePattern} + + // Store unique image IDs to avoid duplicates + imageIDs := make(map[uint]struct{}) + + for _, pattern := range patterns { + re := regexp.MustCompile(pattern) + matches := re.FindAllStringSubmatch(content, -1) + + for _, match := range matches { + if len(match) >= 2 { + if id, err := strconv.ParseUint(match[1], 10, 32); err == nil { + imageIDs[uint(id)] = struct{}{} + } + } + } + } + + // Convert map keys to slice + result := make([]uint, 0, len(imageIDs)) + for id := range imageIDs { + result = append(result, id) + } + + return result +}