package photos import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) type Client struct { baseURL string httpClient *http.Client } func NewClient(baseURL string) *Client { return &Client{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 60 * time.Second, }, } } type Asset struct { ID string `json:"id"` Type string `json:"type"` OriginalPath string `json:"originalPath"` OriginalName string `json:"originalFileName"` MimeType string `json:"originalMimeType"` FileSize int64 `json:"fileSizeInByte"` CreatedAt string `json:"fileCreatedAt"` IsFavorite bool `json:"isFavorite"` ThumbHash string `json:"thumbhash"` ExifInfo struct { FileSizeInByte int64 `json:"fileSizeInByte"` } `json:"exifInfo,omitempty"` } type Album struct { ID string `json:"id"` Name string `json:"albumName"` Description string `json:"description,omitempty"` AssetCount int `json:"assetCount"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt,omitempty"` } const assetsPageSize = 200 func (c *Client) GetAllAssets(ctx context.Context, apiKey string) ([]Asset, error) { var all []Asset page := 1 for { batch, err := c.GetAssets(ctx, apiKey, page, assetsPageSize) if err != nil { return nil, err } all = append(all, batch...) if len(batch) < assetsPageSize { break } page++ } return all, nil } func (c *Client) GetAssets(ctx context.Context, apiKey string, page, size int) ([]Asset, error) { url := fmt.Sprintf("%s/assets?page=%d&size=%d", c.baseURL, page, size) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("x-api-key", apiKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { io.Copy(io.Discard, resp.Body) return nil, &HTTPStatusError{Operation: "get assets", StatusCode: resp.StatusCode} } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var assets []Asset trimmed := bytes.TrimSpace(body) switch { case len(trimmed) == 0: return []Asset{}, nil case trimmed[0] == '[': if err := json.Unmarshal(trimmed, &assets); err != nil { return nil, err } default: var wrapped struct { Items []Asset `json:"items"` Assets []Asset `json:"assets"` } if err := json.Unmarshal(trimmed, &wrapped); err != nil { return nil, err } if len(wrapped.Items) > 0 { assets = wrapped.Items } else { assets = wrapped.Assets } } for i := range assets { if assets[i].FileSize == 0 { assets[i].FileSize = assets[i].ExifInfo.FileSizeInByte } } return assets, nil } func (c *Client) GetAlbums(ctx context.Context, apiKey string) ([]Album, error) { url := fmt.Sprintf("%s/albums", c.baseURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("x-api-key", apiKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { io.Copy(io.Discard, resp.Body) return nil, &HTTPStatusError{Operation: "get albums", StatusCode: resp.StatusCode} } var albums []Album if err := json.NewDecoder(resp.Body).Decode(&albums); err != nil { return nil, err } return albums, nil } func (c *Client) UploadAsset(ctx context.Context, apiKey string, body io.Reader, contentType string) (string, error) { url := fmt.Sprintf("%s/assets", c.baseURL) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return "", err } req.Header.Set("x-api-key", apiKey) req.Header.Set("Content-Type", contentType) resp, err := c.httpClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { io.Copy(io.Discard, resp.Body) return "", &HTTPStatusError{Operation: "upload asset", StatusCode: resp.StatusCode} } var result struct { ID string `json:"id"` AssetID string `json:"assetId"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } if result.ID != "" { return result.ID, nil } return result.AssetID, nil } func (c *Client) GetAssetThumbnail(ctx context.Context, apiKey, assetID string) (io.ReadCloser, string, error) { url := fmt.Sprintf("%s/assets/%s/thumbnail", c.baseURL, assetID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, "", err } req.Header.Set("x-api-key", apiKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, "", err } if resp.StatusCode != http.StatusOK { defer resp.Body.Close() io.Copy(io.Discard, resp.Body) return nil, "", &HTTPStatusError{Operation: "get asset thumbnail", StatusCode: resp.StatusCode} } return resp.Body, resp.Header.Get("Content-Type"), nil } func (c *Client) DeleteAsset(ctx context.Context, apiKey, assetID string) error { url := fmt.Sprintf("%s/assets/%s", c.baseURL, assetID) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) if err != nil { return err } req.Header.Set("x-api-key", apiKey) resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { io.Copy(io.Discard, resp.Body) return &HTTPStatusError{Operation: "delete asset", StatusCode: resp.StatusCode} } return nil }