15 changed files with 766 additions and 147 deletions
@ -0,0 +1,139 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package util_test |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/suite" |
||||
"github.com/superseriousbusiness/gotosocial/internal/config" |
||||
"github.com/superseriousbusiness/gotosocial/internal/util" |
||||
) |
||||
|
||||
type PagingSuite struct { |
||||
suite.Suite |
||||
} |
||||
|
||||
func (suite *PagingSuite) TestPagingStandard() { |
||||
config.SetHost("example.org") |
||||
|
||||
params := util.PageableResponseParams{ |
||||
Items: make([]interface{}, 10, 10), |
||||
Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses", |
||||
NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN", |
||||
PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R", |
||||
Limit: 10, |
||||
} |
||||
|
||||
resp, errWithCode := util.PackagePageableResponse(params) |
||||
if errWithCode != nil { |
||||
suite.FailNow(errWithCode.Error()) |
||||
} |
||||
|
||||
suite.Equal(make([]interface{}, 10, 10), resp.Items) |
||||
suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN>; rel="next", <https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R>; rel="prev"`, resp.LinkHeader) |
||||
suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN`, resp.NextLink) |
||||
suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R`, resp.PrevLink) |
||||
} |
||||
|
||||
func (suite *PagingSuite) TestPagingNoLimit() { |
||||
config.SetHost("example.org") |
||||
|
||||
params := util.PageableResponseParams{ |
||||
Items: make([]interface{}, 10, 10), |
||||
Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses", |
||||
NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN", |
||||
PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R", |
||||
} |
||||
|
||||
resp, errWithCode := util.PackagePageableResponse(params) |
||||
if errWithCode != nil { |
||||
suite.FailNow(errWithCode.Error()) |
||||
} |
||||
|
||||
suite.Equal(make([]interface{}, 10, 10), resp.Items) |
||||
suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?max_id=01H11KA1DM2VH3747YDE7FV5HN>; rel="next", <https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?min_id=01H11KBBVRRDYYC5KEPME1NP5R>; rel="prev"`, resp.LinkHeader) |
||||
suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?max_id=01H11KA1DM2VH3747YDE7FV5HN`, resp.NextLink) |
||||
suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?min_id=01H11KBBVRRDYYC5KEPME1NP5R`, resp.PrevLink) |
||||
} |
||||
|
||||
func (suite *PagingSuite) TestPagingNoNextID() { |
||||
config.SetHost("example.org") |
||||
|
||||
params := util.PageableResponseParams{ |
||||
Items: make([]interface{}, 10, 10), |
||||
Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses", |
||||
PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R", |
||||
Limit: 10, |
||||
} |
||||
|
||||
resp, errWithCode := util.PackagePageableResponse(params) |
||||
if errWithCode != nil { |
||||
suite.FailNow(errWithCode.Error()) |
||||
} |
||||
|
||||
suite.Equal(make([]interface{}, 10, 10), resp.Items) |
||||
suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R>; rel="prev"`, resp.LinkHeader) |
||||
suite.Equal(``, resp.NextLink) |
||||
suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R`, resp.PrevLink) |
||||
} |
||||
|
||||
func (suite *PagingSuite) TestPagingNoPrevID() { |
||||
config.SetHost("example.org") |
||||
|
||||
params := util.PageableResponseParams{ |
||||
Items: make([]interface{}, 10, 10), |
||||
Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses", |
||||
NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN", |
||||
Limit: 10, |
||||
} |
||||
|
||||
resp, errWithCode := util.PackagePageableResponse(params) |
||||
if errWithCode != nil { |
||||
suite.FailNow(errWithCode.Error()) |
||||
} |
||||
|
||||
suite.Equal(make([]interface{}, 10, 10), resp.Items) |
||||
suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN>; rel="next"`, resp.LinkHeader) |
||||
suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN`, resp.NextLink) |
||||
suite.Equal(``, resp.PrevLink) |
||||
} |
||||
|
||||
func (suite *PagingSuite) TestPagingNoItems() { |
||||
config.SetHost("example.org") |
||||
|
||||
params := util.PageableResponseParams{ |
||||
NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN", |
||||
PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R", |
||||
Limit: 10, |
||||
} |
||||
|
||||
resp, errWithCode := util.PackagePageableResponse(params) |
||||
if errWithCode != nil { |
||||
suite.FailNow(errWithCode.Error()) |
||||
} |
||||
|
||||
suite.Empty(resp.Items) |
||||
suite.Empty(resp.LinkHeader) |
||||
suite.Empty(resp.NextLink) |
||||
suite.Empty(resp.PrevLink) |
||||
} |
||||
|
||||
func TestPagingSuite(t *testing.T) { |
||||
suite.Run(t, &PagingSuite{}) |
||||
} |
||||
@ -0,0 +1,149 @@
|
||||
/* |
||||
* MinIO Go Library for Amazon S3 Compatible Cloud Storage |
||||
* Copyright 2023 MinIO, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package minio |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"io" |
||||
"mime/multipart" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// PutObjectFanOutRequest this is the request structure sent
|
||||
// to the server to fan-out the stream to multiple objects.
|
||||
type PutObjectFanOutRequest struct { |
||||
Key string `json:"key"` |
||||
UserMetadata map[string]string `json:"metadata,omitempty"` |
||||
UserTags map[string]string `json:"tags,omitempty"` |
||||
ContentType string `json:"contentType,omitempty"` |
||||
ContentEncoding string `json:"contentEncoding,omitempty"` |
||||
ContentDisposition string `json:"contentDisposition,omitempty"` |
||||
ContentLanguage string `json:"contentLanguage,omitempty"` |
||||
CacheControl string `json:"cacheControl,omitempty"` |
||||
Retention RetentionMode `json:"retention,omitempty"` |
||||
RetainUntilDate *time.Time `json:"retainUntil,omitempty"` |
||||
} |
||||
|
||||
// PutObjectFanOutResponse this is the response structure sent
|
||||
// by the server upon success or failure for each object
|
||||
// fan-out keys. Additionally this response carries ETag,
|
||||
// VersionID and LastModified for each object fan-out.
|
||||
type PutObjectFanOutResponse struct { |
||||
Key string `json:"key"` |
||||
ETag string `json:"etag,omitempty"` |
||||
VersionID string `json:"versionId,omitempty"` |
||||
LastModified *time.Time `json:"lastModified,omitempty"` |
||||
Error error `json:"error,omitempty"` |
||||
} |
||||
|
||||
// PutObjectFanOut - is a variant of PutObject instead of writing a single object from a single
|
||||
// stream multiple objects are written, defined via a list of PutObjectFanOutRequests. Each entry
|
||||
// in PutObjectFanOutRequest carries an object keyname and its relevant metadata if any. `Key` is
|
||||
// mandatory, rest of the other options in PutObjectFanOutRequest are optional.
|
||||
func (c *Client) PutObjectFanOut(ctx context.Context, bucket string, body io.Reader, fanOutReq ...PutObjectFanOutRequest) ([]PutObjectFanOutResponse, error) { |
||||
if len(fanOutReq) == 0 { |
||||
return nil, errInvalidArgument("fan out requests cannot be empty") |
||||
} |
||||
|
||||
policy := NewPostPolicy() |
||||
policy.SetBucket(bucket) |
||||
policy.SetKey(strconv.FormatInt(time.Now().UnixNano(), 16)) |
||||
|
||||
// Expires in 15 minutes.
|
||||
policy.SetExpires(time.Now().UTC().Add(15 * time.Minute)) |
||||
|
||||
url, formData, err := c.PresignedPostPolicy(ctx, policy) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
r, w := io.Pipe() |
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url.String(), r) |
||||
if err != nil { |
||||
w.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
var b strings.Builder |
||||
enc := json.NewEncoder(&b) |
||||
for _, req := range fanOutReq { |
||||
if req.Key == "" { |
||||
w.Close() |
||||
return nil, errors.New("PutObjectFanOutRequest.Key is mandatory and cannot be empty") |
||||
} |
||||
if err = enc.Encode(&req); err != nil { |
||||
w.Close() |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
mwriter := multipart.NewWriter(w) |
||||
req.Header.Add("Content-Type", mwriter.FormDataContentType()) |
||||
|
||||
go func() { |
||||
defer w.Close() |
||||
defer mwriter.Close() |
||||
|
||||
for k, v := range formData { |
||||
if err := mwriter.WriteField(k, v); err != nil { |
||||
return |
||||
} |
||||
} |
||||
|
||||
if err := mwriter.WriteField("x-minio-fanout-list", b.String()); err != nil { |
||||
return |
||||
} |
||||
|
||||
mw, err := mwriter.CreateFormFile("file", "fanout-content") |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if _, err = io.Copy(mw, body); err != nil { |
||||
return |
||||
} |
||||
}() |
||||
|
||||
resp, err := c.do(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer closeResponse(resp) |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, httpRespToErrorResponse(resp, bucket, "fanout-content") |
||||
} |
||||
|
||||
dec := json.NewDecoder(resp.Body) |
||||
fanOutResp := make([]PutObjectFanOutResponse, 0, len(fanOutReq)) |
||||
for dec.More() { |
||||
var m PutObjectFanOutResponse |
||||
if err = dec.Decode(&m); err != nil { |
||||
return nil, err |
||||
} |
||||
fanOutResp = append(fanOutResp, m) |
||||
} |
||||
|
||||
return fanOutResp, nil |
||||
} |
||||
Loading…
Reference in new issue