mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
44c6087dc5
commit
0dafb2eb48
6 files changed
+187
-207
No files matched your search
+39
-61
@@ -22,12 +22,16 @@ class NativeFile {
|
|||||||
if (value.temporary) nativeHost?.resources?.register(this, value.nativeFile);
|
if (value.temporary) nativeHost?.resources?.register(this, value.nativeFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
async arrayBuffer() {
|
async arrayBuffer(signal) {
|
||||||
const response = await fetch(this.url);
|
const response = await fetch(this.url, {signal});
|
||||||
if (!response.ok) throw new Error("读取文件失败");
|
if (!response.ok) throw new Error("读取文件失败");
|
||||||
return response.arrayBuffer();
|
return response.arrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {nativeFile:this.nativeFile, size:this.size, name:this.name};
|
||||||
|
}
|
||||||
|
|
||||||
async text() {
|
async text() {
|
||||||
return new TextDecoder().decode(await this.arrayBuffer());
|
return new TextDecoder().decode(await this.arrayBuffer());
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,10 @@ class NativeHandle {
|
|||||||
Object.assign(this, value);
|
Object.assign(this, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {nativeHandle:this.nativeHandle, name:this.name, kind:this.kind};
|
||||||
|
}
|
||||||
|
|
||||||
async getFile() {
|
async getFile() {
|
||||||
return new NativeFile(await this.host.call("file", {handle:this.nativeHandle}));
|
return new NativeFile(await this.host.call("file", {handle:this.nativeHandle}));
|
||||||
}
|
}
|
||||||
@@ -58,11 +66,7 @@ class NativeHandle {
|
|||||||
async createWritable() {
|
async createWritable() {
|
||||||
const {writer} = await this.host.call("write.begin", {handle:this.nativeHandle});
|
const {writer} = await this.host.call("write.begin", {handle:this.nativeHandle});
|
||||||
return {
|
return {
|
||||||
write: async data => {
|
write: data => this.host.call("write.data", {writer, data}),
|
||||||
const resources = new Set();
|
|
||||||
try { return await this.host.call("write.data", {writer, data:await this.host.encode(data, "", resources)}); }
|
|
||||||
finally { for (const file of resources) await this.host.call("release", {file}).catch(() => {}); }
|
|
||||||
},
|
|
||||||
close: () => this.host.call("write.commit", {writer}),
|
close: () => this.host.call("write.commit", {writer}),
|
||||||
abort: () => this.host.call("write.abort", {writer}),
|
abort: () => this.host.call("write.abort", {writer}),
|
||||||
};
|
};
|
||||||
@@ -77,59 +81,34 @@ class NativeHost {
|
|||||||
}) : null;
|
}) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
call(name, value) {
|
call(name, value, signal) {
|
||||||
return this.bridge.call(`ofdgo.host.${name}`, value);
|
return this.bridge.call(`ofdgo.host.${name}`, value, {signal});
|
||||||
}
|
}
|
||||||
|
|
||||||
async encode(value, key = "", resources = null) {
|
async decode(value, name = "", key = "", signal) {
|
||||||
if (value?.nativeFile) return {nativeFile:value.nativeFile, size:value.size, name:value.name};
|
|
||||||
if (value?.nativeHandle) return {nativeHandle:value.nativeHandle, name:value.name, kind:value.kind};
|
|
||||||
if (value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
||||||
if (!(value instanceof Blob)) {
|
|
||||||
const bytes = ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value);
|
|
||||||
if (bytes.byteLength <= 65536 || ["password", "key", "keyPassword", "certificate"].includes(key)) return {nativeBytes:Array.from(bytes)};
|
|
||||||
}
|
|
||||||
const response = await fetch("/__native/input", {method:"POST", body:value});
|
|
||||||
if (!response.ok) throw new Error("传递数据失败");
|
|
||||||
const file = await response.json();
|
|
||||||
resources?.add(file.nativeFile);
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const results = await Promise.allSettled(value.map(item => this.encode(item, "", resources)));
|
|
||||||
const failure = results.find(item => item.status === "rejected");
|
|
||||||
if (failure) throw failure.reason;
|
|
||||||
return results.map(item => item.value);
|
|
||||||
}
|
|
||||||
if (value && typeof value === "object") {
|
|
||||||
const result = {};
|
|
||||||
for (const [key, item] of Object.entries(value)) result[key] = await this.encode(item, key, resources);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async decode(value, name = "", key = "") {
|
|
||||||
if (value?.nativeFile) {
|
if (value?.nativeFile) {
|
||||||
const file = new NativeFile(value);
|
const file = new NativeFile(value);
|
||||||
if (key === "file" || name === "ofdgoConvertPDF" && key === "bytes") return file;
|
if (key === "file" || name === "ofdgoConvertPDF" && key === "bytes") return file;
|
||||||
try {
|
try {
|
||||||
const bytes = await file.arrayBuffer();
|
const bytes = await file.arrayBuffer(signal);
|
||||||
return key === "blob" ? new Blob([bytes], {type:value.type}) : new Uint8Array(bytes);
|
return key === "blob" ? new Blob([bytes], {type:value.type}) : new Uint8Array(bytes);
|
||||||
} finally {
|
} finally {
|
||||||
await this.call("release", {file:value.nativeFile});
|
await this.call("release", {file:value.nativeFile});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) return Promise.all(value.map(item => this.decode(item, name)));
|
if (Array.isArray(value)) {
|
||||||
|
for (let i = 0; i < value.length; i++) value[i] = await this.decode(value[i], name, "", signal);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
const result = {};
|
for (const [field, item] of Object.entries(value)) value[field] = await this.decode(item, name, field, signal);
|
||||||
for (const [field, item] of Object.entries(value)) result[field] = await this.decode(item, name, field);
|
return value;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async release(value) {
|
async release(value) {
|
||||||
|
if (value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return;
|
||||||
if (value?.nativeFile && value.temporary) {
|
if (value?.nativeFile && value.temporary) {
|
||||||
await this.call("release", {file:value.nativeFile}).catch(() => {});
|
await this.call("release", {file:value.nativeFile}).catch(() => {});
|
||||||
return;
|
return;
|
||||||
@@ -144,11 +123,10 @@ class NativeHost {
|
|||||||
let closed = false;
|
let closed = false;
|
||||||
let unsubscribe;
|
let unsubscribe;
|
||||||
let pending = Promise.resolve();
|
let pending = Promise.resolve();
|
||||||
const canceled = new Set();
|
const requests = new Map();
|
||||||
const resources = new Set();
|
|
||||||
const emit = data => { if (!closed) worker.onmessage?.({data}); };
|
const emit = data => { if (!closed) worker.onmessage?.({data}); };
|
||||||
const ready = host.call("engine.open", {}).then(({engine}) => {
|
const ready = host.call("engine.open", {}).then(({engine}) => {
|
||||||
if (closed) { host.call("engine.close", {engine}).catch(() => {}); return engine; }
|
if (closed) return engine;
|
||||||
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
|
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
|
||||||
emit({type:"ready"});
|
emit({type:"ready"});
|
||||||
return engine;
|
return engine;
|
||||||
@@ -159,35 +137,35 @@ class NativeHost {
|
|||||||
postMessage(data) {
|
postMessage(data) {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
if (data.type === "cancel") {
|
if (data.type === "cancel") {
|
||||||
canceled.add(data.id);
|
requests.get(data.id)?.abort();
|
||||||
ready.then(engine => host.call("engine.cancel", {engine, id:data.id})).catch(() => {});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.type === "font") return;
|
if (data.type === "font") return;
|
||||||
|
const controller = new AbortController();
|
||||||
|
requests.set(data.id, controller);
|
||||||
pending = pending.then(async () => {
|
pending = pending.then(async () => {
|
||||||
const engine = await ready;
|
const engine = await ready;
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
if (canceled.has(data.id)) throw Object.assign(new Error("操作已取消"), {name:"AbortError"});
|
controller.signal.throwIfAborted();
|
||||||
const args = await host.encode(data.args, "", resources);
|
const result = await host.call("engine.call", {engine, id:data.id, name:data.name, args:data.args}, controller.signal);
|
||||||
if (closed) return;
|
|
||||||
if (canceled.has(data.id)) throw Object.assign(new Error("操作已取消"), {name:"AbortError"});
|
|
||||||
const result = await host.call("engine.call", {engine, id:data.id, name:data.name, args});
|
|
||||||
if (closed) { await host.release(result); return; }
|
if (closed) { await host.release(result); return; }
|
||||||
const decoded = await host.decode(result, data.name);
|
let decoded;
|
||||||
|
try {
|
||||||
|
controller.signal.throwIfAborted();
|
||||||
|
decoded = await host.decode(result, data.name, "", controller.signal);
|
||||||
|
controller.signal.throwIfAborted();
|
||||||
|
}
|
||||||
|
catch (error) { await host.release(result); throw error; }
|
||||||
if (closed) { await host.release(result); return; }
|
if (closed) { await host.release(result); return; }
|
||||||
emit({id:data.id, ...decoded});
|
emit({id:data.id, ...decoded});
|
||||||
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => canceled.delete(data.id));
|
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => requests.delete(data.id));
|
||||||
},
|
},
|
||||||
terminate() {
|
terminate() {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
closed = true;
|
closed = true;
|
||||||
unsubscribe?.();
|
unsubscribe?.();
|
||||||
const closing = ready.then(engine => host.call("engine.close", {engine})).catch(() => {});
|
for (const controller of requests.values()) controller.abort();
|
||||||
pending.finally(async () => {
|
ready.then(engine => host.call("engine.close", {engine})).catch(() => {});
|
||||||
await closing;
|
|
||||||
for (const file of resources) await host.call("release", {file}).catch(() => {});
|
|
||||||
resources.clear();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
ready.catch(() => {});
|
ready.catch(() => {});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ require (
|
|||||||
github.com/go-text/typesetting v0.3.5
|
github.com/go-text/typesetting v0.3.5
|
||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
|
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
|
||||||
github.com/xiaoqidun/guigo v0.0.0-20261017125337-3ba9ee2327d9
|
github.com/xiaoqidun/guigo v0.0.0-20261017140706-fa172a358fcb
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2
|
github.com/xiaoqidun/j2kgo v1.0.2
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8
|
github.com/xiaoqidun/jbig2 v1.1.8
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261016003351-18348c283ac1
|
github.com/xiaoqidun/pdfgo v0.0.0-20261016003351-18348c283ac1
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
|
|||||||
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/guigo v0.0.0-20261017125337-3ba9ee2327d9 h1:nEvRfs+yvtx6nJIjsPNhCF6sVmgmNYLKV7ln5cwR2fM=
|
github.com/xiaoqidun/guigo v0.0.0-20261017140706-fa172a358fcb h1:6P+29TxUu6JJX1OqEm/rlCwQ9AjGsWPBsKLNyw/sHUI=
|
||||||
github.com/xiaoqidun/guigo v0.0.0-20261017125337-3ba9ee2327d9/go.mod h1:QAHK2Sa/KvY6dO+6dtvVUeOXVQO4Zh8R5oSSU+av7kE=
|
github.com/xiaoqidun/guigo v0.0.0-20261017140706-fa172a358fcb/go.mod h1:QAHK2Sa/KvY6dO+6dtvVUeOXVQO4Zh8R5oSSU+av7kE=
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw=
|
github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw=
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NativeResolver 将授权标识解析为随机读取源,生命周期由宿主管理
|
// NativeResolver 将授权标识解析为随机读取源,生命周期由宿主管理
|
||||||
|
// 入参: token 资源授权标识
|
||||||
|
// 返回: io.ReaderAt 随机读取器, int64 资源大小, error 解析错误
|
||||||
type NativeResolver func(token string) (io.ReaderAt, int64, error)
|
type NativeResolver func(token string) (io.ReaderAt, int64, error)
|
||||||
|
|
||||||
// NativeOutputRequest 请求为整次批量转换预留唯一输出入口
|
// NativeOutputRequest 请求为整次批量转换预留唯一输出入口
|
||||||
@@ -60,7 +62,9 @@ type NativeOutputTarget struct {
|
|||||||
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
|
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
|
||||||
type NativeEngine struct {
|
type NativeEngine struct {
|
||||||
businessEngine
|
businessEngine
|
||||||
Resolve NativeResolver
|
// Resolve 按授权标识提供资源读取源
|
||||||
|
Resolve NativeResolver
|
||||||
|
// LoadFont 按名称读取系统字体数据
|
||||||
LoadFont func(context.Context, string) ([]byte, error)
|
LoadFont func(context.Context, string) ([]byte, error)
|
||||||
// OpenWriter 必须防止覆盖已有文件,成功时Close提交,失败时仅调用可选Abort
|
// OpenWriter 必须防止覆盖已有文件,成功时Close提交,失败时仅调用可选Abort
|
||||||
// 不支持Abort的输出由宿主负责回收,宿主也负责回滚已完成的批量输出
|
// 不支持Abort的输出由宿主负责回收,宿主也负责回滚已完成的批量输出
|
||||||
@@ -258,30 +262,33 @@ func (r nativeReader) ReadAt(data []byte, offset int64) (int, error) {
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readBytes 读取需要完整数据的资源,每块检查取消
|
// readBytes 分块读取完整资源并检查取消状态
|
||||||
|
// 入参: ctx 调用上下文, value 资源参数
|
||||||
|
// 返回: []byte 资源数据, error 读取错误
|
||||||
func (e *NativeEngine) readBytes(ctx context.Context, value any) ([]byte, error) {
|
func (e *NativeEngine) readBytes(ctx context.Context, value any) ([]byte, error) {
|
||||||
source, size, err := e.source(ctx, value)
|
source, size, err := e.source(ctx, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var output bytes.Buffer
|
if size > int64(int(^uint(0)>>1)) {
|
||||||
chunk := make([]byte, 256<<10)
|
return nil, fmt.Errorf("resource exceeds address space")
|
||||||
|
}
|
||||||
|
output := make([]byte, int(size))
|
||||||
for offset := int64(0); offset < size; {
|
for offset := int64(0); offset < size; {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
n := int(min(int64(len(chunk)), size-offset))
|
n := int(min(int64(256<<10), size-offset))
|
||||||
got, err := source.ReadAt(chunk[:n], offset)
|
got, err := source.ReadAt(output[int(offset):int(offset)+n], offset)
|
||||||
if got != n {
|
if got != n {
|
||||||
return nil, io.ErrUnexpectedEOF
|
return nil, io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
output.Write(chunk[:got])
|
|
||||||
offset += int64(got)
|
offset += int64(got)
|
||||||
}
|
}
|
||||||
return output.Bytes(), nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nativePlain 保留字节并转换JSON结构
|
// nativePlain 保留字节并转换JSON结构
|
||||||
|
|||||||
+87
-90
@@ -20,7 +20,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -49,6 +48,7 @@ type Host struct {
|
|||||||
window *guigo.Window
|
window *guigo.Window
|
||||||
files *fileStore
|
files *fileStore
|
||||||
engines map[string]*webui.NativeEngine
|
engines map[string]*webui.NativeEngine
|
||||||
|
engineStops map[string]func() bool
|
||||||
cancels map[string]context.CancelFunc
|
cancels map[string]context.CancelFunc
|
||||||
fontOnce sync.Once
|
fontOnce sync.Once
|
||||||
fonts []map[string]any
|
fonts []map[string]any
|
||||||
@@ -78,8 +78,8 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
|
|||||||
h := &Host{files: files, engines: make(map[string]*webui.NativeEngine), cancels: make(map[string]context.CancelFunc), fontFiles: make(map[string]string), pendingPaths: append([]string(nil), paths...)}
|
h := &Host{files: files, engines: make(map[string]*webui.NativeEngine), cancels: make(map[string]context.CancelFunc), fontFiles: make(map[string]string), pendingPaths: append([]string(nil), paths...)}
|
||||||
defer h.close()
|
defer h.close()
|
||||||
bindings := make(map[string]guigo.HandlerFunc)
|
bindings := make(map[string]guigo.HandlerFunc)
|
||||||
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.cancel", "open", "save", "directory", "file", "release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
|
for _, name := range []string{"engine.open", "engine.close", "engine.call", "open", "save", "directory", "file", "release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
|
||||||
bindings["ofdgo.host."+name] = func(ctx context.Context, raw json.RawMessage) (any, error) { return h.call(ctx, name, raw) }
|
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
|
||||||
}
|
}
|
||||||
static := http.FileServerFS(webuiassets.NativeFS)
|
static := http.FileServerFS(webuiassets.NativeFS)
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -219,6 +219,7 @@ func (h *Host) resource(r *http.Request) (*guigo.Resource, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// emit 发送当前窗口事件
|
// emit 发送当前窗口事件
|
||||||
|
// 入参: name 事件名称, value 事件数据
|
||||||
func (h *Host) emit(name string, value any) {
|
func (h *Host) emit(name string, value any) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
w := h.window
|
w := h.window
|
||||||
@@ -229,6 +230,7 @@ func (h *Host) emit(name string, value any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// openPaths 将操作系统提供的文件登记后交给界面
|
// openPaths 将操作系统提供的文件登记后交给界面
|
||||||
|
// 入参: names 文件路径列表
|
||||||
func (h *Host) openPaths(names []string) {
|
func (h *Host) openPaths(names []string) {
|
||||||
files := make([]any, 0, len(names))
|
files := make([]any, 0, len(names))
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@@ -246,10 +248,12 @@ func (h *Host) openPaths(names []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// call 执行宿主协议并保留业务结果结构
|
// call 执行宿主协议并保留业务结果结构
|
||||||
func (h *Host) call(ctx context.Context, name string, raw json.RawMessage) (any, error) {
|
// 入参: ctx 调用上下文, name 方法名称, value 已解码参数
|
||||||
var args map[string]any
|
// 返回: any 调用结果, error 执行错误
|
||||||
if err := json.Unmarshal(raw, &args); err != nil {
|
func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
||||||
return nil, err
|
args, ok := value.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("参数无效")
|
||||||
}
|
}
|
||||||
text := func(key string) string { value, _ := args[key].(string); return value }
|
text := func(key string) string { value, _ := args[key].(string); return value }
|
||||||
flag := func(key string) bool { value, _ := args[key].(bool); return value }
|
flag := func(key string) bool { value, _ := args[key].(bool); return value }
|
||||||
@@ -263,32 +267,14 @@ func (h *Host) call(ctx context.Context, name string, raw json.RawMessage) (any,
|
|||||||
id := rand.Text()
|
id := rand.Text()
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
h.engines[id] = engine
|
h.engines[id] = engine
|
||||||
|
if h.engineStops == nil {
|
||||||
|
h.engineStops = make(map[string]func() bool)
|
||||||
|
}
|
||||||
|
h.engineStops[id] = context.AfterFunc(guigo.PageContext(ctx), func() { _ = h.closeEngine(id) })
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
return map[string]any{"engine": id}, nil
|
return guigo.Result{Value: map[string]any{"engine": id}, Discard: func() { _ = h.closeEngine(id) }}, nil
|
||||||
case "engine.close":
|
case "engine.close":
|
||||||
id := text("engine")
|
return nil, h.closeEngine(text("engine"))
|
||||||
h.mu.Lock()
|
|
||||||
engine := h.engines[id]
|
|
||||||
delete(h.engines, id)
|
|
||||||
for key, cancel := range h.cancels {
|
|
||||||
if strings.HasPrefix(key, id+"/") {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.mu.Unlock()
|
|
||||||
if engine != nil {
|
|
||||||
return nil, engine.Close()
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
case "engine.cancel":
|
|
||||||
key := text("engine") + "/" + fmt.Sprint(args["id"])
|
|
||||||
h.mu.Lock()
|
|
||||||
cancel := h.cancels[key]
|
|
||||||
h.mu.Unlock()
|
|
||||||
if cancel != nil {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
case "engine.call":
|
case "engine.call":
|
||||||
id := text("engine")
|
id := text("engine")
|
||||||
key := id + "/" + fmt.Sprint(args["id"])
|
key := id + "/" + fmt.Sprint(args["id"])
|
||||||
@@ -307,20 +293,14 @@ func (h *Host) call(ctx context.Context, name string, raw json.RawMessage) (any,
|
|||||||
return nil, fmt.Errorf("会话已关闭")
|
return nil, fmt.Errorf("会话已关闭")
|
||||||
}
|
}
|
||||||
values, _ := args["args"].([]any)
|
values, _ := args["args"].([]any)
|
||||||
for i, value := range values {
|
|
||||||
var err error
|
|
||||||
values[i], err = nativeBytes(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result := engine.Call(operation, text("name"), values, func(event any) {
|
result := engine.Call(operation, text("name"), values, func(event any) {
|
||||||
if value, ok := event.(map[string]any); ok {
|
if value, ok := event.(map[string]any); ok {
|
||||||
value["id"] = args["id"]
|
value["id"] = args["id"]
|
||||||
h.emit("ofdgo.engine."+id, value)
|
h.emit("ofdgo.engine."+id, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return h.encode(result), nil
|
encoded := h.encode(guigo.PageContext(ctx), result)
|
||||||
|
return guigo.Result{Value: encoded, Discard: func() { h.discard(encoded) }}, nil
|
||||||
case "open", "save", "directory":
|
case "open", "save", "directory":
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
window := h.window
|
window := h.window
|
||||||
@@ -420,13 +400,11 @@ func (h *Host) call(ctx context.Context, name string, raw json.RawMessage) (any,
|
|||||||
if name == "write.abort" {
|
if name == "write.abort" {
|
||||||
return nil, writer.Abort()
|
return nil, writer.Abort()
|
||||||
}
|
}
|
||||||
descriptor, _ := args["data"].(map[string]any)
|
if data, ok := args["data"].([]byte); ok {
|
||||||
if value, err := nativeBytes(descriptor); err != nil {
|
_, err := writer.Write(data)
|
||||||
return nil, err
|
|
||||||
} else if data, ok := value.([]byte); ok {
|
|
||||||
_, err = writer.Write(data)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
descriptor, _ := args["data"].(map[string]any)
|
||||||
key, _ := descriptor["nativeFile"].(string)
|
key, _ := descriptor["nativeFile"].(string)
|
||||||
source, size, err := h.files.resolve(key)
|
source, size, err := h.files.resolve(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -458,48 +436,9 @@ func (h *Host) call(ctx context.Context, name string, raw json.RawMessage) (any,
|
|||||||
return nil, fmt.Errorf("未知宿主操作")
|
return nil, fmt.Errorf("未知宿主操作")
|
||||||
}
|
}
|
||||||
|
|
||||||
// nativeBytes 还原仅在消息内传递的小型二进制参数
|
|
||||||
func nativeBytes(value any) (any, error) {
|
|
||||||
switch value := value.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
if raw, exists := value["nativeBytes"]; exists {
|
|
||||||
items, ok := raw.([]any)
|
|
||||||
if !ok || len(value) != 1 || len(items) > 1<<20 {
|
|
||||||
return nil, fmt.Errorf("二进制参数无效")
|
|
||||||
}
|
|
||||||
data := make([]byte, len(items))
|
|
||||||
for i, item := range items {
|
|
||||||
number, ok := item.(float64)
|
|
||||||
if !ok || number < 0 || number > 255 || float64(byte(number)) != number {
|
|
||||||
return nil, fmt.Errorf("二进制参数无效")
|
|
||||||
}
|
|
||||||
data[i] = byte(number)
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
for key, item := range value {
|
|
||||||
decoded, err := nativeBytes(item)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
value[key] = decoded
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
case []any:
|
|
||||||
for i, item := range value {
|
|
||||||
decoded, err := nativeBytes(item)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
value[i] = decoded
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
default:
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialogFilters 将共用界面的文件类型映射到系统对话框
|
// dialogFilters 将共用界面的文件类型映射到系统对话框
|
||||||
|
// 入参: args 对话框参数
|
||||||
|
// 返回: []guigo.FileFilter 文件类型列表
|
||||||
func dialogFilters(args map[string]any) []guigo.FileFilter {
|
func dialogFilters(args map[string]any) []guigo.FileFilter {
|
||||||
patterns := func(values []string) string {
|
patterns := func(values []string) string {
|
||||||
var result []string
|
var result []string
|
||||||
@@ -571,6 +510,8 @@ func dialogFilters(args map[string]any) []guigo.FileFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dialogError 将用户取消与业务错误区分
|
// dialogError 将用户取消与业务错误区分
|
||||||
|
// 入参: err 对话框错误
|
||||||
|
// 返回: error 页面可识别的错误
|
||||||
func dialogError(err error) error {
|
func dialogError(err error) error {
|
||||||
if errors.Is(err, guigo.ErrCanceled) {
|
if errors.Is(err, guigo.ErrCanceled) {
|
||||||
return &guigo.BridgeError{Name: "AbortError", Message: "已取消"}
|
return &guigo.BridgeError{Name: "AbortError", Message: "已取消"}
|
||||||
@@ -578,21 +519,23 @@ func dialogError(err error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode 将二进制输出转为窗口内资源
|
// encode 将二进制输出登记为页面资源
|
||||||
func (h *Host) encode(value any) any {
|
// 入参: ctx 页面上下文, value 原生调用结果
|
||||||
|
// 返回: any 包含资源描述的调用结果
|
||||||
|
func (h *Host) encode(ctx context.Context, value any) any {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case []byte:
|
case []byte:
|
||||||
return h.files.bytes(v)
|
return h.files.bytes(ctx, v)
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
out := make(map[string]any, len(v))
|
out := make(map[string]any, len(v))
|
||||||
for key, item := range v {
|
for key, item := range v {
|
||||||
out[key] = h.encode(item)
|
out[key] = h.encode(ctx, item)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
case []any:
|
case []any:
|
||||||
out := make([]any, len(v))
|
out := make([]any, len(v))
|
||||||
for i, item := range v {
|
for i, item := range v {
|
||||||
out[i] = h.encode(item)
|
out[i] = h.encode(ctx, item)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
default:
|
default:
|
||||||
@@ -600,7 +543,28 @@ func (h *Host) encode(value any) any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discard 释放页面未接收的临时结果
|
||||||
|
// 入参: value 包含资源描述的调用结果
|
||||||
|
func (h *Host) discard(value any) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
if id, ok := v["nativeFile"].(string); ok && v["temporary"] == true {
|
||||||
|
h.files.release(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, item := range v {
|
||||||
|
h.discard(item)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, item := range v {
|
||||||
|
h.discard(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// openWriter 将授权目标转换为原生事务写入器
|
// openWriter 将授权目标转换为原生事务写入器
|
||||||
|
// 入参: ctx 调用上下文, value 目标句柄, name 输出文件名
|
||||||
|
// 返回: io.WriteCloser 事务写入器, error 创建错误
|
||||||
func (h *Host) openWriter(ctx context.Context, value any, name string) (io.WriteCloser, error) {
|
func (h *Host) openWriter(ctx context.Context, value any, name string) (io.WriteCloser, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -631,6 +595,8 @@ func (h *Host) openWriter(ctx context.Context, value any, name string) (io.Write
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reserveOutput 为一次转换预留独立输出文件或目录
|
// reserveOutput 为一次转换预留独立输出文件或目录
|
||||||
|
// 入参: ctx 调用上下文, request 输出请求
|
||||||
|
// 返回: webui.NativeOutputTarget 预留目标, error 创建错误
|
||||||
func (h *Host) reserveOutput(ctx context.Context, request webui.NativeOutputRequest) (webui.NativeOutputTarget, error) {
|
func (h *Host) reserveOutput(ctx context.Context, request webui.NativeOutputRequest) (webui.NativeOutputTarget, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return webui.NativeOutputTarget{}, err
|
return webui.NativeOutputTarget{}, err
|
||||||
@@ -642,6 +608,8 @@ func (h *Host) reserveOutput(ctx context.Context, request webui.NativeOutputRequ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cleanupOutput 回滚本次转换创建的输出入口
|
// cleanupOutput 回滚本次转换创建的输出入口
|
||||||
|
// 入参: ctx 调用上下文, target 预留目标
|
||||||
|
// 返回: error 清理错误
|
||||||
func (h *Host) cleanupOutput(ctx context.Context, target webui.NativeOutputTarget) error {
|
func (h *Host) cleanupOutput(ctx context.Context, target webui.NativeOutputTarget) error {
|
||||||
handle, _ := target.Destination.(map[string]any)
|
handle, _ := target.Destination.(map[string]any)
|
||||||
id, _ := handle["nativeHandle"].(string)
|
id, _ := handle["nativeHandle"].(string)
|
||||||
@@ -679,6 +647,8 @@ func (h *Host) readFonts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadFont 按转换请求读取单个系统字体
|
// loadFont 按转换请求读取单个系统字体
|
||||||
|
// 入参: ctx 调用上下文, name 字体文件路径及字体索引
|
||||||
|
// 返回: []byte 字体数据, error 读取错误
|
||||||
func (h *Host) loadFont(ctx context.Context, name string) ([]byte, error) {
|
func (h *Host) loadFont(ctx context.Context, name string) ([]byte, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -696,6 +666,29 @@ func (h *Host) loadFont(ctx context.Context, name string) ([]byte, error) {
|
|||||||
return (ofdgo.FontFile{Data: data}).Face(index)
|
return (ofdgo.FontFile{Data: data}).Face(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// closeEngine 取消会话调用并释放文档
|
||||||
|
// 入参: id 会话标识
|
||||||
|
// 返回: error 关闭错误
|
||||||
|
func (h *Host) closeEngine(id string) error {
|
||||||
|
h.mu.Lock()
|
||||||
|
engine := h.engines[id]
|
||||||
|
delete(h.engines, id)
|
||||||
|
if stop := h.engineStops[id]; stop != nil {
|
||||||
|
stop()
|
||||||
|
delete(h.engineStops, id)
|
||||||
|
}
|
||||||
|
for key, cancel := range h.cancels {
|
||||||
|
if strings.HasPrefix(key, id+"/") {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
if engine != nil {
|
||||||
|
return engine.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// close 取消任务并释放当前窗口持有的会话
|
// close 取消任务并释放当前窗口持有的会话
|
||||||
func (h *Host) close() {
|
func (h *Host) close() {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
@@ -703,6 +696,10 @@ func (h *Host) close() {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
engines := h.engines
|
engines := h.engines
|
||||||
|
for _, stop := range h.engineStops {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
h.engineStops = nil
|
||||||
h.engines = make(map[string]*webui.NativeEngine)
|
h.engines = make(map[string]*webui.NativeEngine)
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
for _, engine := range engines {
|
for _, engine := range engines {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ package webview
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -37,6 +37,7 @@ type fileEntry struct {
|
|||||||
name string
|
name string
|
||||||
data []byte
|
data []byte
|
||||||
file *os.File
|
file *os.File
|
||||||
|
stop func() bool
|
||||||
directory bool
|
directory bool
|
||||||
owned bool
|
owned bool
|
||||||
temporary bool
|
temporary bool
|
||||||
@@ -45,7 +46,6 @@ type fileEntry struct {
|
|||||||
// fileStore 管理窗口范围内的文件授权和临时数据
|
// fileStore 管理窗口范围内的文件授权和临时数据
|
||||||
type fileStore struct {
|
type fileStore struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
transfers sync.WaitGroup
|
|
||||||
entries map[string]*fileEntry
|
entries map[string]*fileEntry
|
||||||
writers map[string]*fileWriter
|
writers map[string]*fileWriter
|
||||||
temporary string
|
temporary string
|
||||||
@@ -64,6 +64,7 @@ type fileWriter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newFileStore 创建独立文件授权表
|
// newFileStore 创建独立文件授权表
|
||||||
|
// 返回: *fileStore 文件授权表, error 创建错误
|
||||||
func newFileStore() (*fileStore, error) {
|
func newFileStore() (*fileStore, error) {
|
||||||
dir, err := temporaryDirectory()
|
dir, err := temporaryDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -110,6 +111,8 @@ func temporaryFile(dir, prefix string) (*os.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add 登记已有授权文件
|
// add 登记已有授权文件
|
||||||
|
// 入参: name 文件路径, directory 是否为目录
|
||||||
|
// 返回: map[string]any 句柄描述, error 登记错误
|
||||||
func (s *fileStore) add(name string, directory bool) (map[string]any, error) {
|
func (s *fileStore) add(name string, directory bool) (map[string]any, error) {
|
||||||
info, err := os.Stat(name)
|
info, err := os.Stat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -126,6 +129,8 @@ func (s *fileStore) add(name string, directory bool) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save 登记用户选择的输出路径
|
// save 登记用户选择的输出路径
|
||||||
|
// 入参: name 输出文件路径
|
||||||
|
// 返回: map[string]any 句柄描述
|
||||||
func (s *fileStore) save(name string) map[string]any {
|
func (s *fileStore) save(name string) map[string]any {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -135,6 +140,8 @@ func (s *fileStore) save(name string) map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// descriptor 返回文件句柄,不暴露本机路径
|
// descriptor 返回文件句柄,不暴露本机路径
|
||||||
|
// 入参: id 授权标识
|
||||||
|
// 返回: map[string]any 句柄描述
|
||||||
func (s *fileStore) descriptor(id string) map[string]any {
|
func (s *fileStore) descriptor(id string) map[string]any {
|
||||||
entry := s.entries[id]
|
entry := s.entries[id]
|
||||||
kind := "file"
|
kind := "file"
|
||||||
@@ -145,6 +152,8 @@ func (s *fileStore) descriptor(id string) map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// file 创建独立读取快照并返回资源描述
|
// file 创建独立读取快照并返回资源描述
|
||||||
|
// 入参: id 授权标识
|
||||||
|
// 返回: map[string]any 资源描述, error 读取错误
|
||||||
func (s *fileStore) file(id string) (map[string]any, error) {
|
func (s *fileStore) file(id string) (map[string]any, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -172,6 +181,8 @@ func (s *fileStore) file(id string) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolve 按授权标识读取文件并保持随机读取句柄
|
// resolve 按授权标识读取文件并保持随机读取句柄
|
||||||
|
// 入参: id 授权标识
|
||||||
|
// 返回: io.ReaderAt 随机读取器, int64 文件大小, error 读取错误
|
||||||
func (s *fileStore) resolve(id string) (io.ReaderAt, int64, error) {
|
func (s *fileStore) resolve(id string) (io.ReaderAt, int64, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -196,15 +207,20 @@ func (s *fileStore) resolve(id string) (io.ReaderAt, int64, error) {
|
|||||||
return entry.file, info.Size(), nil
|
return entry.file, info.Size(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// bytes 登记原生输出,前端通过资源通道读取
|
// bytes 登记原生输出,页面退出时释放未读取数据
|
||||||
func (s *fileStore) bytes(data []byte) map[string]any {
|
// 入参: ctx 页面上下文, data 输出数据
|
||||||
|
// 返回: map[string]any 资源描述,页面取消或授权表关闭时返回nil
|
||||||
|
func (s *fileStore) bytes(ctx context.Context, data []byte) map[string]any {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = []byte{}
|
data = []byte{}
|
||||||
}
|
}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
if s.closed || ctx.Err() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
id := rand.Text()
|
id := rand.Text()
|
||||||
s.entries[id] = &fileEntry{data: data, name: "data"}
|
s.entries[id] = &fileEntry{data: data, name: "data", stop: context.AfterFunc(ctx, func() { s.release(id) })}
|
||||||
return map[string]any{"nativeFile": id, "size": len(data), "name": "data", "url": "/__native/data/" + id, "temporary": true}
|
return map[string]any{"nativeFile": id, "size": len(data), "name": "data", "url": "/__native/data/" + id, "temporary": true}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +249,16 @@ func (s *fileStore) resource(id string) (io.ReaderAt, int64, func() error, error
|
|||||||
return file, info.Size(), file.Close, nil
|
return file, info.Size(), file.Close, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// release 释放已传递完成的内存结果
|
// release 释放内存结果或临时文件并取消页面清理回调
|
||||||
|
// 入参: id 资源标识
|
||||||
func (s *fileStore) release(id string) {
|
func (s *fileStore) release(id string) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary) {
|
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary) {
|
||||||
delete(s.entries, id)
|
delete(s.entries, id)
|
||||||
|
if entry.stop != nil {
|
||||||
|
entry.stop()
|
||||||
|
}
|
||||||
if entry.file != nil {
|
if entry.file != nil {
|
||||||
entry.file.Close()
|
entry.file.Close()
|
||||||
}
|
}
|
||||||
@@ -249,6 +269,8 @@ func (s *fileStore) release(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// child 在已授权目录中查找或创建直接子项
|
// child 在已授权目录中查找或创建直接子项
|
||||||
|
// 入参: id 目录授权标识, name 子项名称, directory 是否为目录, create 是否允许创建
|
||||||
|
// 返回: map[string]any 句柄描述, error 访问错误
|
||||||
func (s *fileStore) child(id, name string, directory, create bool) (map[string]any, error) {
|
func (s *fileStore) child(id, name string, directory, create bool) (map[string]any, error) {
|
||||||
if !fs.ValidPath(name) || name == "." || strings.ContainsAny(name, "/\\:") {
|
if !fs.ValidPath(name) || name == "." || strings.ContainsAny(name, "/\\:") {
|
||||||
return nil, fmt.Errorf("文件名无效")
|
return nil, fmt.Errorf("文件名无效")
|
||||||
@@ -302,6 +324,8 @@ func (s *fileStore) child(id, name string, directory, create bool) (map[string]a
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reserve 为批量输出独占创建入口并避开已有文件
|
// reserve 为批量输出独占创建入口并避开已有文件
|
||||||
|
// 入参: id 目录授权标识, base 基础名称, extension 扩展名, directory 是否创建目录
|
||||||
|
// 返回: string 输出基础名称, string 输出入口名称, error 创建错误
|
||||||
func (s *fileStore) reserve(id, base, extension string, directory bool) (string, string, error) {
|
func (s *fileStore) reserve(id, base, extension string, directory bool) (string, string, error) {
|
||||||
if !fs.ValidPath(base) || base == "." || strings.ContainsAny(base+extension, "/\\:") {
|
if !fs.ValidPath(base) || base == "." || strings.ContainsAny(base+extension, "/\\:") {
|
||||||
return "", "", fmt.Errorf("文件名无效")
|
return "", "", fmt.Errorf("文件名无效")
|
||||||
@@ -350,6 +374,8 @@ func (s *fileStore) reserve(id, base, extension string, directory bool) (string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// begin 创建文件写入事务
|
// begin 创建文件写入事务
|
||||||
|
// 入参: id 输出授权标识
|
||||||
|
// 返回: *fileWriter 事务写入器, error 创建错误
|
||||||
func (s *fileStore) begin(id string) (*fileWriter, error) {
|
func (s *fileStore) begin(id string) (*fileWriter, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -370,6 +396,8 @@ func (s *fileStore) begin(id string) (*fileWriter, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove 清理由当前窗口创建的目录子项
|
// remove 清理由当前窗口创建的目录子项
|
||||||
|
// 入参: id 目录授权标识, name 子项名称, recursive 是否递归删除
|
||||||
|
// 返回: error 删除错误
|
||||||
func (s *fileStore) remove(id, name string, recursive bool) error {
|
func (s *fileStore) remove(id, name string, recursive bool) error {
|
||||||
if !fs.ValidPath(name) || name == "." || strings.ContainsAny(name, "/\\:") {
|
if !fs.ValidPath(name) || name == "." || strings.ContainsAny(name, "/\\:") {
|
||||||
return fmt.Errorf("文件名无效")
|
return fmt.Errorf("文件名无效")
|
||||||
@@ -414,6 +442,8 @@ func (s *fileStore) remove(id, name string, recursive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write 追加输出数据
|
// Write 追加输出数据
|
||||||
|
// 入参: data 输出数据
|
||||||
|
// 返回: int 写入字节数, error 写入错误
|
||||||
func (w *fileWriter) Write(data []byte) (int, error) {
|
func (w *fileWriter) Write(data []byte) (int, error) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
@@ -432,6 +462,7 @@ func (w *fileWriter) Write(data []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close 同步并替换目标文件
|
// Close 同步并替换目标文件
|
||||||
|
// 返回: error 提交错误
|
||||||
func (w *fileWriter) Close() error {
|
func (w *fileWriter) Close() error {
|
||||||
defer w.release()
|
defer w.release()
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
@@ -460,6 +491,7 @@ func (w *fileWriter) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Abort 删除未提交的临时输出
|
// Abort 删除未提交的临时输出
|
||||||
|
// 返回: error 清理错误
|
||||||
func (w *fileWriter) Abort() error {
|
func (w *fileWriter) Abort() error {
|
||||||
defer w.release()
|
defer w.release()
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
@@ -479,46 +511,9 @@ func (w *fileWriter) release() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// serve 提供窗口内资源,不建立网络监听器
|
// serve 提供窗口内资源,不建立网络监听器
|
||||||
|
// 入参: w 响应写入器, r 资源请求
|
||||||
func (s *fileStore) serve(w http.ResponseWriter, r *http.Request) {
|
func (s *fileStore) serve(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
if r.URL.Path == "/__native/input" && r.Method == http.MethodPost {
|
|
||||||
s.mu.Lock()
|
|
||||||
if s.closed {
|
|
||||||
s.mu.Unlock()
|
|
||||||
http.Error(w, os.ErrClosed.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.transfers.Add(1)
|
|
||||||
s.mu.Unlock()
|
|
||||||
defer s.transfers.Done()
|
|
||||||
file, err := temporaryFile(s.temporary, "input-")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
size, err := io.Copy(file, r.Body)
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
os.Remove(file.Name())
|
|
||||||
http.Error(w, err.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
if s.closed {
|
|
||||||
s.mu.Unlock()
|
|
||||||
file.Close()
|
|
||||||
os.Remove(file.Name())
|
|
||||||
http.Error(w, os.ErrClosed.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := rand.Text()
|
|
||||||
s.entries[id] = &fileEntry{path: file.Name(), name: "data", file: file, temporary: true}
|
|
||||||
s.mu.Unlock()
|
|
||||||
value := map[string]any{"nativeFile": id, "size": size, "name": "data", "url": "/__native/data/" + id, "temporary": true}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(r.URL.Path, "/__native/data/") || r.Method != http.MethodGet && r.Method != http.MethodHead {
|
if !strings.HasPrefix(r.URL.Path, "/__native/data/") || r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -540,6 +535,7 @@ func (s *fileStore) serve(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// close 释放授权文件与未提交输出
|
// close 释放授权文件与未提交输出
|
||||||
|
// 返回: error 清理错误
|
||||||
func (s *fileStore) close() error {
|
func (s *fileStore) close() error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.closed {
|
if s.closed {
|
||||||
@@ -552,12 +548,14 @@ func (s *fileStore) close() error {
|
|||||||
s.writers = make(map[string]*fileWriter)
|
s.writers = make(map[string]*fileWriter)
|
||||||
s.entries = make(map[string]*fileEntry)
|
s.entries = make(map[string]*fileEntry)
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
s.transfers.Wait()
|
|
||||||
var err error
|
var err error
|
||||||
for _, writer := range writers {
|
for _, writer := range writers {
|
||||||
err = errors.Join(err, writer.Abort())
|
err = errors.Join(err, writer.Abort())
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
if entry.stop != nil {
|
||||||
|
entry.stop()
|
||||||
|
}
|
||||||
if entry.file != nil {
|
if entry.file != nil {
|
||||||
err = errors.Join(err, entry.file.Close())
|
err = errors.Join(err, entry.file.Close())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user