Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
52a5ca2e
Commit
52a5ca2e
authored
Jun 23, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: update get attachment binary
parent
697e5475
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
120 additions
and
0 deletions
+120
-0
attachment_service.go
server/router/api/v1/attachment_service.go
+120
-0
No files found.
server/router/api/v1/attachment_service.go
View file @
52a5ca2e
...
@@ -10,6 +10,7 @@ import (
...
@@ -10,6 +10,7 @@ import (
"os"
"os"
"path/filepath"
"path/filepath"
"regexp"
"regexp"
"strconv"
"strings"
"strings"
"time"
"time"
...
@@ -17,7 +18,9 @@ import (
...
@@ -17,7 +18,9 @@ import (
"github.com/lithammer/shortuuid/v4"
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
"github.com/pkg/errors"
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/timestamppb"
...
@@ -276,6 +279,44 @@ func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.Ge
...
@@ -276,6 +279,44 @@ func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.Ge
contentType
=
"application/octet-stream"
contentType
=
"application/octet-stream"
}
}
// Extract range header from gRPC metadata for iOS Safari video support
var
rangeHeader
string
if
md
,
ok
:=
metadata
.
FromIncomingContext
(
ctx
);
ok
{
// Check for range header from gRPC-Gateway
if
ranges
:=
md
.
Get
(
"grpcgateway-range"
);
len
(
ranges
)
>
0
{
rangeHeader
=
ranges
[
0
]
}
else
if
ranges
:=
md
.
Get
(
"range"
);
len
(
ranges
)
>
0
{
rangeHeader
=
ranges
[
0
]
}
// Log for debugging iOS Safari issues
if
userAgents
:=
md
.
Get
(
"user-agent"
);
len
(
userAgents
)
>
0
{
userAgent
:=
userAgents
[
0
]
if
strings
.
Contains
(
strings
.
ToLower
(
userAgent
),
"safari"
)
&&
rangeHeader
!=
""
{
slog
.
Debug
(
"Safari range request detected"
,
slog
.
String
(
"range"
,
rangeHeader
),
slog
.
String
(
"user-agent"
,
userAgent
),
slog
.
String
(
"content-type"
,
contentType
))
}
}
}
// Handle range requests for video/audio streaming (iOS Safari requirement)
if
rangeHeader
!=
""
&&
(
strings
.
HasPrefix
(
contentType
,
"video/"
)
||
strings
.
HasPrefix
(
contentType
,
"audio/"
))
{
return
s
.
handleRangeRequest
(
ctx
,
blob
,
rangeHeader
,
contentType
)
}
// Set headers for streaming support
if
strings
.
HasPrefix
(
contentType
,
"video/"
)
||
strings
.
HasPrefix
(
contentType
,
"audio/"
)
{
if
err
:=
setResponseHeaders
(
ctx
,
map
[
string
]
string
{
"accept-ranges"
:
"bytes"
,
"content-length"
:
fmt
.
Sprintf
(
"%d"
,
len
(
blob
)),
"cache-control"
:
"public, max-age=3600"
,
// 1 hour cache
});
err
!=
nil
{
slog
.
Warn
(
"failed to set streaming headers"
,
slog
.
Any
(
"error"
,
err
))
}
}
return
&
httpbody
.
HttpBody
{
return
&
httpbody
.
HttpBody
{
ContentType
:
contentType
,
ContentType
:
contentType
,
Data
:
blob
,
Data
:
blob
,
...
@@ -551,3 +592,82 @@ func replaceFilenameWithPathTemplate(path, filename string) string {
...
@@ -551,3 +592,82 @@ func replaceFilenameWithPathTemplate(path, filename string) string {
})
})
return
path
return
path
}
}
// handleRangeRequest handles HTTP range requests for video/audio streaming (iOS Safari requirement).
func
(
*
APIV1Service
)
handleRangeRequest
(
ctx
context
.
Context
,
data
[]
byte
,
rangeHeader
,
contentType
string
)
(
*
httpbody
.
HttpBody
,
error
)
{
// Parse "bytes=start-end"
if
!
strings
.
HasPrefix
(
rangeHeader
,
"bytes="
)
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid range header format"
)
}
rangeSpec
:=
strings
.
TrimPrefix
(
rangeHeader
,
"bytes="
)
parts
:=
strings
.
Split
(
rangeSpec
,
"-"
)
if
len
(
parts
)
!=
2
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid range specification"
)
}
fileSize
:=
int64
(
len
(
data
))
start
,
end
:=
int64
(
0
),
fileSize
-
1
// Parse start position
if
parts
[
0
]
!=
""
{
if
s
,
err
:=
strconv
.
ParseInt
(
parts
[
0
],
10
,
64
);
err
==
nil
{
start
=
s
}
else
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid range start: %s"
,
parts
[
0
])
}
}
// Parse end position
if
parts
[
1
]
!=
""
{
if
e
,
err
:=
strconv
.
ParseInt
(
parts
[
1
],
10
,
64
);
err
==
nil
{
end
=
e
}
else
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid range end: %s"
,
parts
[
1
])
}
}
// Validate range
if
start
<
0
||
end
>=
fileSize
||
start
>
end
{
// Set Content-Range header for 416 response
if
err
:=
setResponseHeaders
(
ctx
,
map
[
string
]
string
{
"content-range"
:
fmt
.
Sprintf
(
"bytes */%d"
,
fileSize
),
});
err
!=
nil
{
slog
.
Warn
(
"failed to set content-range header"
,
slog
.
Any
(
"error"
,
err
))
}
return
nil
,
status
.
Errorf
(
codes
.
OutOfRange
,
"requested range not satisfiable"
)
}
// Set partial content headers (HTTP 206)
if
err
:=
setResponseHeaders
(
ctx
,
map
[
string
]
string
{
"accept-ranges"
:
"bytes"
,
"content-range"
:
fmt
.
Sprintf
(
"bytes %d-%d/%d"
,
start
,
end
,
fileSize
),
"content-length"
:
fmt
.
Sprintf
(
"%d"
,
end
-
start
+
1
),
"cache-control"
:
"public, max-age=3600"
,
});
err
!=
nil
{
slog
.
Warn
(
"failed to set partial content headers"
,
slog
.
Any
(
"error"
,
err
))
}
// Extract the requested range
rangeData
:=
data
[
start
:
end
+
1
]
slog
.
Debug
(
"serving partial content"
,
slog
.
Int64
(
"start"
,
start
),
slog
.
Int64
(
"end"
,
end
),
slog
.
Int64
(
"total"
,
fileSize
),
slog
.
Int
(
"chunk_size"
,
len
(
rangeData
)))
return
&
httpbody
.
HttpBody
{
ContentType
:
contentType
,
Data
:
rangeData
,
},
nil
}
// setResponseHeaders is a helper function to set gRPC response headers.
func
setResponseHeaders
(
ctx
context
.
Context
,
headers
map
[
string
]
string
)
error
{
pairs
:=
make
([]
string
,
0
,
len
(
headers
)
*
2
)
for
key
,
value
:=
range
headers
{
pairs
=
append
(
pairs
,
key
,
value
)
}
return
grpc
.
SetHeader
(
ctx
,
metadata
.
Pairs
(
pairs
...
))
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment