mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 15:49:49 +00:00
478 lines
11 KiB
Go
478 lines
11 KiB
Go
package stream
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/dop251/goja_nodejs/eventloop"
|
|
)
|
|
|
|
//go:embed stream.js
|
|
var script string
|
|
|
|
type Config struct {
|
|
CreateObjectURL func(opts *ObjectURLOptions, open ObjectURLOpener) (string, error)
|
|
RevokeObjectURL func(url string) error
|
|
}
|
|
|
|
type ObjectURLOptions struct {
|
|
ContentType string
|
|
Size int64
|
|
Range bool
|
|
}
|
|
|
|
type ObjectURLOpenRequest struct {
|
|
Offset int64
|
|
End int64
|
|
}
|
|
|
|
type ObjectURLOpener func(ctx context.Context, req ObjectURLOpenRequest) (io.ReadCloser, error)
|
|
|
|
func Enable(runtime *goja.Runtime, loop *eventloop.EventLoop, cfg *Config) error {
|
|
if cfg == nil {
|
|
cfg = &Config{}
|
|
}
|
|
blobPipes := newBlobPipeRegistry()
|
|
if err := runtime.Set("__gopeed_create_blob_object_url", func(call goja.FunctionCall) goja.Value {
|
|
if cfg.CreateObjectURL == nil {
|
|
panic(runtime.NewGoError(fmt.Errorf("blob object url handler not configured")))
|
|
}
|
|
openValue := call.Argument(0)
|
|
if _, ok := goja.AssertFunction(openValue); !ok {
|
|
panic(runtime.NewGoError(fmt.Errorf("blob opener must be callable")))
|
|
}
|
|
optsObj := call.Argument(1).ToObject(runtime)
|
|
size := int64(0)
|
|
contentType := ""
|
|
rangeEnabled := false
|
|
if optsObj != nil {
|
|
size = optsObj.Get("size").ToInteger()
|
|
contentType = optsObj.Get("contentType").String()
|
|
rangeEnabled = optsObj.Get("range").ToBoolean()
|
|
}
|
|
if size < 0 {
|
|
size = 0
|
|
}
|
|
opts := &ObjectURLOptions{
|
|
ContentType: contentType,
|
|
Size: size,
|
|
Range: rangeEnabled,
|
|
}
|
|
url, err := cfg.CreateObjectURL(opts, func(ctx context.Context, req ObjectURLOpenRequest) (io.ReadCloser, error) {
|
|
return openBlobObjectURLReader(ctx, loop, blobPipes, openValue, req)
|
|
})
|
|
if err != nil {
|
|
panic(runtime.NewGoError(err))
|
|
}
|
|
return runtime.ToValue(url)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := runtime.Set("__gopeed_revoke_object_url", func(url string) {
|
|
if cfg.RevokeObjectURL == nil {
|
|
panic(runtime.NewGoError(fmt.Errorf("blob revoke handler not configured")))
|
|
}
|
|
if err := cfg.RevokeObjectURL(url); err != nil {
|
|
panic(runtime.NewGoError(err))
|
|
}
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := runtime.Set("__gopeed_blob_pipe_chunk", func(call goja.FunctionCall) goja.Value {
|
|
id := call.Argument(0).String()
|
|
ok := blobPipes.Push(id, exportBytes(call.Argument(1)))
|
|
return runtime.ToValue(ok)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := runtime.Set("__gopeed_blob_pipe_close", func(id string) {
|
|
blobPipes.Close(id, nil)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := runtime.Set("__gopeed_blob_pipe_error", func(id string, message string) {
|
|
blobPipes.Close(id, errors.New(message))
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
_, err := runtime.RunString(script)
|
|
return err
|
|
}
|
|
|
|
func openBlobObjectURLReader(ctx context.Context, loop *eventloop.EventLoop, pipes *blobPipeRegistry, openValue goja.Value, req ObjectURLOpenRequest) (io.ReadCloser, error) {
|
|
if pipes == nil {
|
|
return nil, fmt.Errorf("blob pipe registry not configured")
|
|
}
|
|
pipe := newBlobPipe(ctx, loop, pipes)
|
|
pipes.Add(pipe)
|
|
_, err := runOnLoop(loop, func(runtime *goja.Runtime) (goja.Value, error) {
|
|
fnVal := runtime.Get("__gopeed_blob_pipe_source")
|
|
fn, ok := goja.AssertFunction(fnVal)
|
|
if !ok {
|
|
return nil, fmt.Errorf("blob pipe helper is not callable")
|
|
}
|
|
request := map[string]any{
|
|
"offset": req.Offset,
|
|
"end": req.End,
|
|
}
|
|
return fn(nil, openValue, runtime.ToValue(request), runtime.ToValue(pipe.id))
|
|
})
|
|
if err != nil {
|
|
pipe.Close()
|
|
return nil, err
|
|
}
|
|
return pipe, nil
|
|
}
|
|
|
|
type blobPipeRegistry struct {
|
|
mu sync.Mutex
|
|
pipes map[string]*blobPipe
|
|
next atomic.Uint64
|
|
}
|
|
|
|
func newBlobPipeRegistry() *blobPipeRegistry {
|
|
return &blobPipeRegistry{
|
|
pipes: map[string]*blobPipe{},
|
|
}
|
|
}
|
|
|
|
func (r *blobPipeRegistry) Add(pipe *blobPipe) {
|
|
r.mu.Lock()
|
|
r.pipes[pipe.id] = pipe
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
func (r *blobPipeRegistry) Remove(id string) {
|
|
r.mu.Lock()
|
|
delete(r.pipes, id)
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
func (r *blobPipeRegistry) Push(id string, chunk []byte) bool {
|
|
r.mu.Lock()
|
|
pipe := r.pipes[id]
|
|
r.mu.Unlock()
|
|
if pipe == nil {
|
|
return false
|
|
}
|
|
return pipe.Push(chunk)
|
|
}
|
|
|
|
func (r *blobPipeRegistry) Close(id string, err error) {
|
|
r.mu.Lock()
|
|
pipe := r.pipes[id]
|
|
r.mu.Unlock()
|
|
if pipe != nil {
|
|
pipe.CloseWithError(err)
|
|
}
|
|
}
|
|
|
|
func (r *blobPipeRegistry) NewID() string {
|
|
return fmt.Sprintf("pipe-%d", r.next.Add(1))
|
|
}
|
|
|
|
type blobPipeItem struct {
|
|
chunk []byte
|
|
err error
|
|
done bool
|
|
}
|
|
|
|
type blobPipe struct {
|
|
id string
|
|
loop *eventloop.EventLoop
|
|
registry *blobPipeRegistry
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
ch chan blobPipeItem
|
|
|
|
mu sync.Mutex
|
|
buf []byte
|
|
closed bool
|
|
once sync.Once
|
|
}
|
|
|
|
func newBlobPipe(ctx context.Context, loop *eventloop.EventLoop, registry *blobPipeRegistry) *blobPipe {
|
|
pipeCtx, cancel := context.WithCancel(ctx)
|
|
return &blobPipe{
|
|
id: registry.NewID(),
|
|
loop: loop,
|
|
registry: registry,
|
|
ctx: pipeCtx,
|
|
cancel: cancel,
|
|
ch: make(chan blobPipeItem, 8),
|
|
}
|
|
}
|
|
|
|
func (p *blobPipe) Push(chunk []byte) bool {
|
|
if len(chunk) == 0 {
|
|
return true
|
|
}
|
|
buf := append([]byte(nil), chunk...)
|
|
select {
|
|
case p.ch <- blobPipeItem{chunk: buf}:
|
|
return true
|
|
case <-p.ctx.Done():
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (p *blobPipe) CloseWithError(err error) {
|
|
p.once.Do(func() {
|
|
p.registry.Remove(p.id)
|
|
select {
|
|
case p.ch <- blobPipeItem{done: true, err: err}:
|
|
case <-p.ctx.Done():
|
|
}
|
|
})
|
|
}
|
|
|
|
func (p *blobPipe) Read(dst []byte) (int, error) {
|
|
p.mu.Lock()
|
|
if len(p.buf) > 0 {
|
|
n := copy(dst, p.buf)
|
|
p.buf = p.buf[n:]
|
|
p.mu.Unlock()
|
|
return n, nil
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
select {
|
|
case item := <-p.ch:
|
|
if item.done {
|
|
if item.err != nil {
|
|
return 0, item.err
|
|
}
|
|
return 0, io.EOF
|
|
}
|
|
n := copy(dst, item.chunk)
|
|
if n < len(item.chunk) {
|
|
p.mu.Lock()
|
|
p.buf = append(p.buf, item.chunk[n:]...)
|
|
p.mu.Unlock()
|
|
}
|
|
return n, nil
|
|
case <-p.ctx.Done():
|
|
return 0, p.ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (p *blobPipe) Close() error {
|
|
p.mu.Lock()
|
|
if p.closed {
|
|
p.mu.Unlock()
|
|
return nil
|
|
}
|
|
p.closed = true
|
|
p.mu.Unlock()
|
|
p.registry.Remove(p.id)
|
|
p.cancel()
|
|
p.cancelSourceReader()
|
|
return nil
|
|
}
|
|
|
|
func (p *blobPipe) cancelSourceReader() {
|
|
if p.loop == nil {
|
|
return
|
|
}
|
|
// Invoke the JavaScript cancellation before returning from Close. The
|
|
// source session may be released immediately after the HTTP handler closes
|
|
// this reader, so merely queueing the callback could let the engine stop
|
|
// before reader.cancel() is called.
|
|
_, _ = runOnLoop(p.loop, func(runtime *goja.Runtime) (goja.Value, error) {
|
|
fnVal := runtime.Get("__gopeed_blob_cancel_pipe_source")
|
|
fn, ok := goja.AssertFunction(fnVal)
|
|
if !ok {
|
|
return goja.Undefined(), nil
|
|
}
|
|
_, err := fn(nil, runtime.ToValue(p.id), runtime.ToValue("blob request closed"))
|
|
return goja.Undefined(), err
|
|
})
|
|
}
|
|
|
|
func exportBytes(value goja.Value) []byte {
|
|
if value == nil || goja.IsNull(value) || goja.IsUndefined(value) {
|
|
return nil
|
|
}
|
|
if ab, ok := value.Export().(goja.ArrayBuffer); ok {
|
|
return ab.Bytes()
|
|
}
|
|
if b, ok := value.Export().([]byte); ok {
|
|
return b
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type blobObjectURLReader struct {
|
|
ctx context.Context
|
|
loop *eventloop.EventLoop
|
|
id string
|
|
|
|
mu sync.Mutex
|
|
buf []byte
|
|
closed bool
|
|
}
|
|
|
|
func (r *blobObjectURLReader) Read(p []byte) (int, error) {
|
|
r.mu.Lock()
|
|
if len(r.buf) > 0 {
|
|
n := copy(p, r.buf)
|
|
r.buf = r.buf[n:]
|
|
r.mu.Unlock()
|
|
return n, nil
|
|
}
|
|
closed := r.closed
|
|
r.mu.Unlock()
|
|
if closed {
|
|
return 0, io.EOF
|
|
}
|
|
if err := r.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
chunkSize := len(p)
|
|
if chunkSize <= 0 {
|
|
chunkSize = 32 * 1024
|
|
}
|
|
value, err := runOnLoop(r.loop, func(runtime *goja.Runtime) (goja.Value, error) {
|
|
fnVal := runtime.Get("__gopeed_blob_read_source")
|
|
fn, ok := goja.AssertFunction(fnVal)
|
|
if !ok {
|
|
return nil, fmt.Errorf("blob read helper is not callable")
|
|
}
|
|
return fn(nil, runtime.ToValue(r.id), runtime.ToValue(chunkSize))
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if goja.IsNull(value) || goja.IsUndefined(value) {
|
|
return 0, io.EOF
|
|
}
|
|
var chunk []byte
|
|
if ab, ok := value.Export().(goja.ArrayBuffer); ok {
|
|
chunk = ab.Bytes()
|
|
} else if b, ok := value.Export().([]byte); ok {
|
|
chunk = b
|
|
} else {
|
|
return 0, fmt.Errorf("blob read helper returned %T", value.Export())
|
|
}
|
|
if len(chunk) == 0 {
|
|
return 0, nil
|
|
}
|
|
n := copy(p, chunk)
|
|
if n < len(chunk) {
|
|
r.mu.Lock()
|
|
r.buf = append(r.buf, chunk[n:]...)
|
|
r.mu.Unlock()
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (r *blobObjectURLReader) Close() error {
|
|
r.mu.Lock()
|
|
if r.closed {
|
|
r.mu.Unlock()
|
|
return nil
|
|
}
|
|
r.closed = true
|
|
r.mu.Unlock()
|
|
_, err := runOnLoop(r.loop, func(runtime *goja.Runtime) (goja.Value, error) {
|
|
fnVal := runtime.Get("__gopeed_blob_close_source")
|
|
fn, ok := goja.AssertFunction(fnVal)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return fn(nil, runtime.ToValue(r.id))
|
|
})
|
|
return err
|
|
}
|
|
|
|
func runOnLoop(loop *eventloop.EventLoop, fn func(runtime *goja.Runtime) (goja.Value, error)) (goja.Value, error) {
|
|
type result struct {
|
|
value goja.Value
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
ok := loop.RunOnLoop(func(runtime *goja.Runtime) {
|
|
send := func(value goja.Value, err error) {
|
|
select {
|
|
case ch <- result{value: value, err: err}:
|
|
default:
|
|
}
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
switch v := r.(type) {
|
|
case error:
|
|
send(nil, v)
|
|
case goja.Value:
|
|
send(nil, exportJSError(v))
|
|
default:
|
|
send(nil, fmt.Errorf("panic: %v", r))
|
|
}
|
|
}
|
|
}()
|
|
value, err := fn(runtime)
|
|
if err != nil {
|
|
send(nil, err)
|
|
return
|
|
}
|
|
if promise, ok := value.Export().(*goja.Promise); ok {
|
|
switch promise.State() {
|
|
case goja.PromiseStateFulfilled:
|
|
send(promise.Result(), nil)
|
|
return
|
|
case goja.PromiseStateRejected:
|
|
send(nil, exportJSError(promise.Result()))
|
|
return
|
|
default:
|
|
thenVal := value.ToObject(runtime).Get("then")
|
|
thenFn, ok := goja.AssertFunction(thenVal)
|
|
if !ok {
|
|
send(nil, errors.New("promise.then is not callable"))
|
|
return
|
|
}
|
|
onFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
|
|
send(call.Argument(0), nil)
|
|
return goja.Undefined()
|
|
})
|
|
onRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
|
|
send(nil, exportJSError(call.Argument(0)))
|
|
return goja.Undefined()
|
|
})
|
|
if _, err := thenFn(value, onFulfilled, onRejected); err != nil {
|
|
send(nil, err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
send(value, nil)
|
|
})
|
|
if !ok {
|
|
return nil, errors.New("engine loop terminated")
|
|
}
|
|
res := <-ch
|
|
return res.value, res.err
|
|
}
|
|
|
|
func exportJSError(value goja.Value) error {
|
|
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) {
|
|
return errors.New("promise rejected")
|
|
}
|
|
if err, ok := value.Export().(error); ok {
|
|
return err
|
|
}
|
|
stack := value.String()
|
|
if ro, ok := value.(*goja.Object); ok {
|
|
stackVal := ro.Get("stack")
|
|
if stackVal != nil && stackVal.String() != "" {
|
|
stack = stackVal.String()
|
|
}
|
|
}
|
|
return errors.New(stack)
|
|
}
|