feat(archive): add support for 7z and bz2 / extract rar and 7zip files protected with password (#2668)

This commit is contained in:
Aaron Liu
2025-08-21 10:20:13 +08:00
parent a1ce16bd5e
commit 91717b7c49
6 changed files with 248 additions and 87 deletions

View File

@@ -163,6 +163,10 @@ type (
rsc io.ReadCloser
pos int64
o *EntitySourceOptions
// Cache for resetRequest URL and expiry
cachedUrl string
cachedExpiry time.Time
}
)
@@ -215,6 +219,10 @@ func NewEntitySource(
}
func (f *entitySource) Apply(opts ...EntitySourceOption) {
if len(opts) > 0 {
// Clear cache when options are applied as they might affect URL generation
f.clearUrlCache()
}
for _, opt := range opts {
opt.Apply(f.o)
}
@@ -247,6 +255,10 @@ func (f *entitySource) LocalPath(ctx context.Context) string {
}
func (f *entitySource) Serve(w http.ResponseWriter, r *http.Request, opts ...EntitySourceOption) {
if len(opts) > 0 {
// Clear cache when options are applied as they might affect URL generation
f.clearUrlCache()
}
for _, opt := range opts {
opt.Apply(f.o)
}
@@ -478,16 +490,22 @@ func (f *entitySource) Read(p []byte) (n int, err error) {
}
func (f *entitySource) ReadAt(p []byte, off int64) (n int, err error) {
if f.IsLocal() {
if f.rsc == nil {
err = f.resetRequest()
}
if readAt, ok := f.rsc.(io.ReaderAt); ok {
return readAt.ReadAt(p, off)
if f.rsc == nil {
err = f.resetRequest()
if err != nil {
return 0, err
}
}
if readAt, ok := f.rsc.(io.ReaderAt); ok {
return readAt.ReadAt(p, off)
}
return 0, errors.New("source does not support ReadAt")
// For non-local sources, use HTTP range request to read at specific offset
rsc, err := f.getRsc(off)
if err != nil {
return 0, err
}
return io.ReadFull(rsc, p)
}
func (f *entitySource) Seek(offset int64, whence int) (int64, error) {
@@ -524,6 +542,12 @@ func (f *entitySource) Close() error {
return nil
}
// clearUrlCache clears the cached URL and expiry
func (f *entitySource) clearUrlCache() {
f.cachedUrl = ""
f.cachedExpiry = time.Time{}
}
func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool {
for _, opt := range opts {
opt.Apply(f.o)
@@ -534,6 +558,10 @@ func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool {
}
func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*EntityUrl, error) {
if len(opts) > 0 {
// Clear cache when options are applied as they might affect URL generation
f.clearUrlCache()
}
for _, opt := range opts {
opt.Apply(f.o)
}
@@ -613,50 +641,75 @@ func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*En
func (f *entitySource) resetRequest() error {
// For inbound files, we can use the handler to open the file directly
if f.IsLocal() {
if f.rsc == nil {
file, err := f.handler.Open(f.o.Ctx, f.e.Source())
if err != nil {
return fmt.Errorf("failed to open inbound file: %w", err)
}
if f.pos > 0 {
_, err = file.Seek(f.pos, io.SeekStart)
if err != nil {
return fmt.Errorf("failed to seek inbound file: %w", err)
}
}
f.rsc = file
if f.o.SpeedLimit > 0 {
bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit)
f.rsc = lrs{f.rsc, ratelimit.Reader(f.rsc, bucket)}
}
}
if f.IsLocal() && f.rsc != nil {
return nil
}
expire := time.Now().Add(defaultUrlExpire)
u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire))
rsc, err := f.getRsc(f.pos)
if err != nil {
return fmt.Errorf("failed to generate download url: %w", err)
return fmt.Errorf("failed to get rsc: %w", err)
}
f.rsc = rsc
return nil
}
func (f *entitySource) getRsc(pos int64) (io.ReadCloser, error) {
// For inbound files, we can use the handler to open the file directly
if f.IsLocal() {
file, err := f.handler.Open(f.o.Ctx, f.e.Source())
if err != nil {
return nil, fmt.Errorf("failed to open inbound file: %w", err)
}
if pos > 0 {
_, err = file.Seek(pos, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("failed to seek inbound file: %w", err)
}
}
if f.o.SpeedLimit > 0 {
bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit)
return lrs{f.rsc, ratelimit.Reader(f.rsc, bucket)}, nil
} else {
return file, nil
}
}
var urlStr string
now := time.Now()
// Check if we have a valid cached URL and expiry
if f.cachedUrl != "" && now.Before(f.cachedExpiry.Add(-time.Minute)) {
// Use cached URL if it's still valid (with 1 minute buffer before expiry)
urlStr = f.cachedUrl
} else {
// Generate new URL and cache it
expire := now.Add(defaultUrlExpire)
u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire))
if err != nil {
return nil, fmt.Errorf("failed to generate download url: %w", err)
}
// Cache the URL and expiry
f.cachedUrl = u.Url
f.cachedExpiry = expire
urlStr = u.Url
}
h := http.Header{}
h.Set("Range", fmt.Sprintf("bytes=%d-", f.pos))
resp := f.c.Request(http.MethodGet, u.Url, nil,
h.Set("Range", fmt.Sprintf("bytes=%d-", pos))
resp := f.c.Request(http.MethodGet, urlStr, nil,
request.WithContext(f.o.Ctx),
request.WithLogger(f.l),
request.WithHeader(h),
).CheckHTTPResponse(http.StatusOK, http.StatusPartialContent)
if resp.Err != nil {
return fmt.Errorf("failed to request download url: %w", resp.Err)
return nil, fmt.Errorf("failed to request download url: %w", resp.Err)
}
f.rsc = resp.Response.Body
return nil
return resp.Response.Body, nil
}
// capExpireTime make sure expire time is not too long or too short (if min or max is set)