mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-05 21:46:26 +00:00
Compare commits
5 Commits
ef12326975
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 115d825234 | |||
| 237063d9fc | |||
| c2215d8589 | |||
| 82cb58dff1 | |||
| b592d6a2f0 |
13
README.md
13
README.md
@@ -9,19 +9,6 @@ Single binary, two modes:
|
|||||||
- `fs server` starts the server explicitly
|
- `fs server` starts the server explicitly
|
||||||
- `fs admin ...` runs admin CLI commands
|
- `fs admin ...` runs admin CLI commands
|
||||||
|
|
||||||
## Versioning and Releases
|
|
||||||
|
|
||||||
- Versioning follows SemVer tags: `vMAJOR.MINOR.PATCH` (example: `v0.4.2`).
|
|
||||||
- `fs version` shows build metadata (`version`, `commit`, `date`).
|
|
||||||
- Pushing a tag like `v0.4.2` triggers Docker image build/push via GitHub Actions.
|
|
||||||
- Published images: `ghcr.io/<owner>/<repo>:v0.4.2` and related semver tags.
|
|
||||||
|
|
||||||
Tag release example:
|
|
||||||
```bash
|
|
||||||
git tag v0.1.0
|
|
||||||
git push origin v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Bucket operations:
|
Bucket operations:
|
||||||
|
|||||||
39
api/api.go
39
api/api.go
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@@ -400,13 +401,21 @@ func shouldDecodeAWSChunkedPayload(r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
signingMode := strings.ToLower(r.Header.Get("x-amz-content-sha256"))
|
signingMode := strings.ToLower(r.Header.Get("x-amz-content-sha256"))
|
||||||
return strings.HasPrefix(signingMode, "streaming-aws4-hmac-sha256-payload")
|
if strings.HasPrefix(signingMode, "streaming-aws4-hmac-sha256-payload") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(signingMode, "streaming-unsigned-payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAWSChunkedDecodingReader(src io.Reader) io.ReadCloser {
|
func newAWSChunkedDecodingReader(src io.Reader) io.ReadCloser {
|
||||||
|
probedReader, isAWSChunked := probeAWSChunkedPayload(src)
|
||||||
|
if !isAWSChunked {
|
||||||
|
return io.NopCloser(probedReader)
|
||||||
|
}
|
||||||
|
|
||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
if err := decodeAWSChunkedPayload(src, pw); err != nil {
|
if err := decodeAWSChunkedPayload(probedReader, pw); err != nil {
|
||||||
_ = pw.CloseWithError(err)
|
_ = pw.CloseWithError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -415,6 +424,30 @@ func newAWSChunkedDecodingReader(src io.Reader) io.ReadCloser {
|
|||||||
return pr
|
return pr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func probeAWSChunkedPayload(src io.Reader) (io.Reader, bool) {
|
||||||
|
reader := bufio.NewReaderSize(src, 512)
|
||||||
|
headerLine, err := reader.ReadSlice('\n')
|
||||||
|
replay := io.MultiReader(bytes.NewReader(headerLine), reader)
|
||||||
|
if err != nil {
|
||||||
|
return replay, false
|
||||||
|
}
|
||||||
|
|
||||||
|
line := strings.TrimRight(string(headerLine), "\r\n")
|
||||||
|
chunkSizeToken := line
|
||||||
|
if idx := strings.IndexByte(chunkSizeToken, ';'); idx >= 0 {
|
||||||
|
chunkSizeToken = chunkSizeToken[:idx]
|
||||||
|
}
|
||||||
|
chunkSizeToken = strings.TrimSpace(chunkSizeToken)
|
||||||
|
if chunkSizeToken == "" {
|
||||||
|
return replay, false
|
||||||
|
}
|
||||||
|
size, parseErr := strconv.ParseInt(chunkSizeToken, 16, 64)
|
||||||
|
if parseErr != nil || size < 0 {
|
||||||
|
return replay, false
|
||||||
|
}
|
||||||
|
return replay, true
|
||||||
|
}
|
||||||
|
|
||||||
func decodeAWSChunkedPayload(src io.Reader, dst io.Writer) error {
|
func decodeAWSChunkedPayload(src io.Reader, dst io.Writer) error {
|
||||||
reader := bufio.NewReader(src)
|
reader := bufio.NewReader(src)
|
||||||
for {
|
for {
|
||||||
@@ -488,7 +521,7 @@ func (h *Handler) handlePutBucket(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeMappedS3Error(w, r, err)
|
writeMappedS3Error(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleDeleteBucket(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleDeleteBucket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -259,7 +259,57 @@ func canonicalPath(u *url.URL) string {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
return "/"
|
return "/"
|
||||||
}
|
}
|
||||||
return path
|
return awsEncodePath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func awsEncodePath(path string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(path))
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
ch := path[i]
|
||||||
|
if ch == '/' || isUnreserved(ch) {
|
||||||
|
b.WriteByte(ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '%' && i+2 < len(path) && isHex(path[i+1]) && isHex(path[i+2]) {
|
||||||
|
b.WriteByte('%')
|
||||||
|
b.WriteByte(toUpperHex(path[i+1]))
|
||||||
|
b.WriteByte(toUpperHex(path[i+2]))
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteByte('%')
|
||||||
|
b.WriteByte(hexUpper(ch >> 4))
|
||||||
|
b.WriteByte(hexUpper(ch & 0x0F))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnreserved(ch byte) bool {
|
||||||
|
return (ch >= 'A' && ch <= 'Z') ||
|
||||||
|
(ch >= 'a' && ch <= 'z') ||
|
||||||
|
(ch >= '0' && ch <= '9') ||
|
||||||
|
ch == '-' || ch == '_' || ch == '.' || ch == '~'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHex(ch byte) bool {
|
||||||
|
return (ch >= '0' && ch <= '9') ||
|
||||||
|
(ch >= 'a' && ch <= 'f') ||
|
||||||
|
(ch >= 'A' && ch <= 'F')
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUpperHex(ch byte) byte {
|
||||||
|
if ch >= 'a' && ch <= 'f' {
|
||||||
|
return ch - ('a' - 'A')
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexUpper(nibble byte) byte {
|
||||||
|
if nibble < 10 {
|
||||||
|
return '0' + nibble
|
||||||
|
}
|
||||||
|
return 'A' + (nibble - 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryPair struct {
|
type queryPair struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user