mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
1 parent
6a4b322e2c
commit
151ddb0a20
21 files changed
+819
-195
No files matched your search
@@ -0,0 +1,216 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrChanged 表示目标文件已被其他操作修改
|
||||
var ErrChanged = errors.New("file changed since opening")
|
||||
|
||||
// ErrBusy 表示目标文件正在写入
|
||||
var ErrBusy = errors.New("file save is in progress")
|
||||
|
||||
// activeFiles 协调当前进程内的目标写入
|
||||
var activeFiles sync.Map
|
||||
|
||||
// Target 保存文件路径及最近确认的文件状态,可跨次写入复用
|
||||
type Target struct {
|
||||
mu sync.Mutex
|
||||
name string
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
// Writer 写入同目录临时文件,提交前检查取消及目标冲突
|
||||
type Writer struct {
|
||||
mu sync.Mutex
|
||||
target *Target
|
||||
file *os.File
|
||||
ctx context.Context
|
||||
err error
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewTarget 记录目标当前状态,允许目标尚不存在
|
||||
// 入参: name 目标路径
|
||||
// 返回: *Target 保存目标, error 路径或文件错误
|
||||
func NewTarget(name string) (*Target, error) {
|
||||
if name == "" {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
name, err := filepath.Abs(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := os.Lstat(name)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
if info != nil && !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("save target is not a regular file")
|
||||
}
|
||||
return &Target{name: name, info: info}, nil
|
||||
}
|
||||
|
||||
// Name 返回目标绝对路径
|
||||
// 返回: string 文件路径
|
||||
func (t *Target) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
// check 检查目标是否保持最近确认的状态
|
||||
// 返回: error 文件状态错误
|
||||
func (t *Target) check() error {
|
||||
info, err := os.Lstat(t.name)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if t.info == nil {
|
||||
return nil
|
||||
}
|
||||
return ErrChanged
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.info == nil || !os.SameFile(t.info, info) || t.info.Size() != info.Size() || !t.info.ModTime().Equal(info.ModTime()) {
|
||||
return ErrChanged
|
||||
}
|
||||
if info.Mode().Perm()&0222 == 0 {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Begin 创建独占写入事务,调用方必须提交或终止
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: *Writer 事务, error 创建错误
|
||||
func (t *Target) Begin(ctx context.Context) (*Writer, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if _, loaded := activeFiles.LoadOrStore(t.name, true); loaded {
|
||||
return nil, ErrBusy
|
||||
}
|
||||
if err := t.check(); err != nil {
|
||||
activeFiles.Delete(t.name)
|
||||
return nil, err
|
||||
}
|
||||
for range 100 {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(10000000))
|
||||
if err != nil {
|
||||
activeFiles.Delete(t.name)
|
||||
return nil, err
|
||||
}
|
||||
name := fmt.Sprintf("%s.%07d.tmp", t.name, n.Int64())
|
||||
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
activeFiles.Delete(t.name)
|
||||
return nil, err
|
||||
}
|
||||
return &Writer{target: t, file: f, ctx: ctx}, nil
|
||||
}
|
||||
activeFiles.Delete(t.name)
|
||||
return nil, os.ErrExist
|
||||
}
|
||||
|
||||
// Write 追加数据并记录写入错误
|
||||
// 入参: data 数据
|
||||
// 返回: int 写入长度, error 写入错误
|
||||
func (w *Writer) Write(data []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
if w.err == nil {
|
||||
w.err = w.ctx.Err()
|
||||
}
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
n, err := w.file.Write(data)
|
||||
if err == nil && n != len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
w.err = err
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close 完成写入并替换目标,成功后刷新目标状态
|
||||
// 返回: error 提交错误
|
||||
func (w *Writer) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return os.ErrClosed
|
||||
}
|
||||
w.closed = true
|
||||
defer activeFiles.Delete(w.target.name)
|
||||
defer os.Remove(w.file.Name())
|
||||
w.target.mu.Lock()
|
||||
defer w.target.mu.Unlock()
|
||||
if err := errors.Join(w.err, w.ctx.Err(), w.target.check()); err != nil {
|
||||
return errors.Join(err, w.file.Close())
|
||||
}
|
||||
if w.target.info != nil {
|
||||
if err := w.file.Chmod(w.target.info.Mode().Perm()); err != nil {
|
||||
return errors.Join(err, w.file.Close())
|
||||
}
|
||||
}
|
||||
if err := w.file.Sync(); err != nil {
|
||||
return errors.Join(err, w.file.Close())
|
||||
}
|
||||
info, err := w.file.Stat()
|
||||
if err = errors.Join(err, w.file.Close(), w.ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.target.check(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := Replace(w.file.Name(), w.target.name); err != nil {
|
||||
return err
|
||||
}
|
||||
w.target.info = info
|
||||
if current, err := os.Stat(w.target.name); err == nil {
|
||||
w.target.info = current
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Abort 丢弃临时输出,不改变目标
|
||||
// 返回: error 清理错误
|
||||
func (w *Writer) Abort() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
w.closed = true
|
||||
defer activeFiles.Delete(w.target.name)
|
||||
return errors.Join(w.file.Close(), os.Remove(w.file.Name()))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package file
|
||||
|
||||
import "os"
|
||||
|
||||
// Open 打开文件,路径替换后已有读取句柄仍有效
|
||||
// 入参: name 文件路径
|
||||
// 返回: *os.File 文件, error 打开错误
|
||||
func Open(name string) (*os.File, error) {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
// Replace 替换目标路径
|
||||
// 入参: source 临时路径, target 目标路径
|
||||
// 返回: error 替换错误
|
||||
func Replace(source, target string) error {
|
||||
return os.Rename(source, target)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// replaceFileProc 保留目标文件属性并替换文件内容
|
||||
var replaceFileProc = syscall.NewLazyDLL("kernel32.dll").NewProc("ReplaceFileW")
|
||||
|
||||
// Open 保留替换共享权限,路径替换后已有读取句柄仍有效
|
||||
// 入参: name 文件路径
|
||||
// 返回: *os.File 文件, error 打开错误
|
||||
func Open(name string) (*os.File, error) {
|
||||
path, err := nativePath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, err := syscall.CreateFile(path, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return os.NewFile(uintptr(handle), name), nil
|
||||
}
|
||||
|
||||
// Replace 替换目标,目标不存在时重命名
|
||||
// 入参: source 临时路径, target 目标路径
|
||||
// 返回: error 替换错误
|
||||
func Replace(source, target string) error {
|
||||
from, err := nativePath(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := nativePath(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, _, err := replaceFileProc.Call(uintptr(unsafe.Pointer(to)), uintptr(unsafe.Pointer(from)), 0, 0, 0, 0)
|
||||
if result != 0 {
|
||||
return nil
|
||||
}
|
||||
if err == syscall.ERROR_FILE_NOT_FOUND {
|
||||
return os.Rename(source, target)
|
||||
}
|
||||
return &os.LinkError{Op: "replace", Old: source, New: target, Err: err}
|
||||
}
|
||||
|
||||
// nativePath 转换绝对长路径
|
||||
// 入参: name 路径
|
||||
// 返回: *uint16 原生路径, error 转换错误
|
||||
func nativePath(name string) (*uint16, error) {
|
||||
path, err := filepath.Abs(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(path, `\\?\`) {
|
||||
if strings.HasPrefix(path, `\\`) {
|
||||
path = `\\?\UNC\` + path[2:]
|
||||
} else {
|
||||
path = `\\?\` + path
|
||||
}
|
||||
}
|
||||
return syscall.UTF16PtrFromString(path)
|
||||
}
|
||||
@@ -419,6 +419,7 @@ type outputContext struct {
|
||||
type businessEngine struct {
|
||||
currentSession *Session
|
||||
currentEditor *ofdgo.Editor
|
||||
saveSnapshot *ofdgo.Editor
|
||||
pendingImport *ofdgo.Reader
|
||||
copiedObjects *editorClipboard
|
||||
copiedStyle *ofdgo.GraphicObject
|
||||
@@ -7162,6 +7163,54 @@ func (e *businessEngine) saveSigned(args []platformValue) (any, error) {
|
||||
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
|
||||
}
|
||||
|
||||
// prepareSave 创建独立保存快照,后续写出不占用当前编辑文档
|
||||
// 入参: args 取消检查回调
|
||||
// 返回: any 快照修订, error 准备错误
|
||||
func (e *businessEngine) prepareSave(args []platformValue) (any, error) {
|
||||
e.saveSnapshot = nil
|
||||
editor := e.currentEditor
|
||||
if editor == nil {
|
||||
if e.currentSession == nil {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
var err error
|
||||
editor, err = e.currentSession.Reader.Editor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx := e.operationContext()
|
||||
if len(args) > 0 {
|
||||
ctx = e.newOutputContext(args[0])
|
||||
}
|
||||
snapshot, err := editor.Snapshot(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.saveSnapshot = snapshot
|
||||
return successResult(map[string]any{"revision": editor.Revision()}), nil
|
||||
}
|
||||
|
||||
// savePrepared 写出并释放已准备的快照,不修改当前编辑状态
|
||||
// 入参: args 写出回调、进度回调及输出配置
|
||||
// 返回: any 保存结果, error 保存错误
|
||||
func (e *businessEngine) savePrepared(args []platformValue) (any, error) {
|
||||
snapshot := e.saveSnapshot
|
||||
e.saveSnapshot = nil
|
||||
if snapshot == nil {
|
||||
return nil, fmt.Errorf("save snapshot is not prepared")
|
||||
}
|
||||
return e.saveEditor(snapshot, args)
|
||||
}
|
||||
|
||||
// discardSave 释放尚未写出的保存快照
|
||||
// 入参: args 空参数
|
||||
// 返回: any 操作结果, error 错误信息
|
||||
func (e *businessEngine) discardSave(args []platformValue) (any, error) {
|
||||
e.saveSnapshot = nil
|
||||
return successResult(nil), nil
|
||||
}
|
||||
|
||||
// saveDocument 分块写出当前编辑文档或指定页面
|
||||
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
|
||||
// 返回: any 保存结果, error 错误信息
|
||||
@@ -7403,6 +7452,9 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
|
||||
"ofdgoUndo": func([]platformValue) (any, error) { return e.restoreEditor(false) },
|
||||
"ofdgoRedo": func([]platformValue) (any, error) { return e.restoreEditor(true) },
|
||||
"ofdgoSaveDocument": e.saveDocument,
|
||||
"ofdgoPrepareSave": e.prepareSave,
|
||||
"ofdgoSaveSnapshot": e.savePrepared,
|
||||
"ofdgoDiscardSave": e.discardSave,
|
||||
"ofdgoSetEncryption": e.setEncryption,
|
||||
"ofdgoSaveSigned": e.saveSigned,
|
||||
"ofdgoPreviewSignatureSeal": e.previewSignatureSeal,
|
||||
|
||||
@@ -82,6 +82,7 @@ type NativeEngine struct {
|
||||
CleanupOutput func(context.Context, NativeOutputTarget) error
|
||||
gate chan struct{}
|
||||
once sync.Once
|
||||
saves sync.WaitGroup
|
||||
closed bool
|
||||
}
|
||||
|
||||
@@ -149,13 +150,33 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
|
||||
case <-ctx.Done():
|
||||
return nativeFailure(ctx.Err())
|
||||
}
|
||||
defer func() { <-e.gate }()
|
||||
locked := true
|
||||
defer func() {
|
||||
if locked {
|
||||
<-e.gate
|
||||
}
|
||||
}()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nativeFailure(err)
|
||||
}
|
||||
if e.closed {
|
||||
return nativeFailure(fmt.Errorf("native engine is closed"))
|
||||
}
|
||||
if name == "ofdgoSaveSnapshot" {
|
||||
snapshot := e.saveSnapshot
|
||||
e.saveSnapshot = nil
|
||||
if snapshot == nil {
|
||||
return nativeFailure(fmt.Errorf("save snapshot is not prepared"))
|
||||
}
|
||||
output := &NativeEngine{OpenWriter: e.OpenWriter}
|
||||
output.currentEditor = snapshot
|
||||
e.saves.Add(1)
|
||||
defer e.saves.Done()
|
||||
<-e.gate
|
||||
locked = false
|
||||
defer output.Close()
|
||||
return output.Call(ctx, "ofdgoSaveDocument", args, emit)
|
||||
}
|
||||
e.ctx = ctx
|
||||
if e.currentSession != nil {
|
||||
e.currentSession.Renderer.OutputContext = ctx
|
||||
@@ -219,6 +240,8 @@ func (e *NativeEngine) Close() error {
|
||||
e.gate <- struct{}{}
|
||||
defer func() { <-e.gate }()
|
||||
e.closed = true
|
||||
e.saveSnapshot = nil
|
||||
e.saves.Wait()
|
||||
var err error
|
||||
if e.currentSession != nil {
|
||||
err = e.currentSession.Close()
|
||||
@@ -728,7 +751,7 @@ func (c *nativeCall) prepare(name string, args []any) ([]any, error) {
|
||||
args = append(args, c.fileCallback(files))
|
||||
}
|
||||
switch name {
|
||||
case "ofdgoResources":
|
||||
case "ofdgoResources", "ofdgoPrepareSave":
|
||||
return append(args, checkpoint), nil
|
||||
case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages":
|
||||
return append(args, c.progress("import", "", true)), nil
|
||||
|
||||
@@ -56,6 +56,8 @@ func RunWASM() {
|
||||
"ofdgoImportPages": true,
|
||||
"ofdgoImportDocuments": true,
|
||||
"ofdgoSaveDocument": true,
|
||||
"ofdgoPrepareSave": true,
|
||||
"ofdgoSaveSnapshot": true,
|
||||
"ofdgoSetEncryption": true,
|
||||
"ofdgoSaveSigned": true,
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
||||
case "remove":
|
||||
return nil, h.files.remove(text("handle"), text("name"), flag("recursive"))
|
||||
case "write.begin":
|
||||
writer, err := h.files.begin(text("handle"))
|
||||
writer, err := h.files.begin(guigo.PageContext(ctx), text("handle"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -660,7 +660,7 @@ func (h *Host) openWriter(ctx context.Context, value any, name string) (io.Write
|
||||
return nil, fmt.Errorf("输出文件已存在")
|
||||
}
|
||||
}
|
||||
return h.files.begin(id)
|
||||
return h.files.begin(ctx, id)
|
||||
}
|
||||
|
||||
// reserveOutput 为一次转换预留独立输出文件或目录
|
||||
|
||||
@@ -29,18 +29,23 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
basefile "github.com/xiaoqidun/ofdgo/internal/base/file"
|
||||
)
|
||||
|
||||
// fileEntry 保存窗口授权的文件或目录
|
||||
type fileEntry struct {
|
||||
path string
|
||||
name string
|
||||
data []byte
|
||||
file *os.File
|
||||
stop func() bool
|
||||
directory bool
|
||||
owned bool
|
||||
temporary bool
|
||||
path string
|
||||
name string
|
||||
data []byte
|
||||
file *os.File
|
||||
stop func() bool
|
||||
directory bool
|
||||
owned bool
|
||||
temporary bool
|
||||
saveTarget *basefile.Target
|
||||
readers int
|
||||
retired bool
|
||||
}
|
||||
|
||||
// fileStore 管理窗口范围内的文件授权和临时数据
|
||||
@@ -55,12 +60,8 @@ type fileStore struct {
|
||||
// fileWriter 在同目录临时文件中保存输出
|
||||
type fileWriter struct {
|
||||
id string
|
||||
mu sync.Mutex
|
||||
store *fileStore
|
||||
file *os.File
|
||||
target string
|
||||
err error
|
||||
closed bool
|
||||
output *basefile.Writer
|
||||
}
|
||||
|
||||
// newFileStore 创建独立文件授权表
|
||||
@@ -124,7 +125,14 @@ func (s *fileStore) add(name string, directory bool) (map[string]any, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
id := rand.Text()
|
||||
s.entries[id] = &fileEntry{path: name, name: filepath.Base(name), directory: directory}
|
||||
entry := &fileEntry{path: name, name: filepath.Base(name), directory: directory}
|
||||
if !directory {
|
||||
entry.saveTarget, err = basefile.NewTarget(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.entries[id] = entry
|
||||
return s.descriptor(id), nil
|
||||
}
|
||||
|
||||
@@ -161,9 +169,10 @@ func (s *fileStore) file(id string) (map[string]any, error) {
|
||||
if entry == nil || entry.directory {
|
||||
return nil, fmt.Errorf("文件授权无效")
|
||||
}
|
||||
handle := s.descriptor(id)
|
||||
size := int64(len(entry.data))
|
||||
if entry.data == nil {
|
||||
file, err := openReadFile(entry.path)
|
||||
file, err := basefile.Open(entry.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -177,7 +186,7 @@ func (s *fileStore) file(id string) (map[string]any, error) {
|
||||
entry = &fileEntry{path: entry.path, name: entry.name, file: file}
|
||||
s.entries[id] = entry
|
||||
}
|
||||
return map[string]any{"nativeFile": id, "name": entry.name, "size": size, "url": "/__native/data/" + id}, nil
|
||||
return map[string]any{"nativeFile": id, "name": entry.name, "size": size, "url": "/__native/data/" + id, "handle": handle}, nil
|
||||
}
|
||||
|
||||
// resolve 按授权标识读取文件并保持随机读取句柄
|
||||
@@ -194,7 +203,7 @@ func (s *fileStore) resolve(id string) (io.ReaderAt, int64, error) {
|
||||
return bytes.NewReader(entry.data), int64(len(entry.data)), nil
|
||||
}
|
||||
if entry.file == nil {
|
||||
file, err := openReadFile(entry.path)
|
||||
file, err := basefile.Open(entry.path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -237,7 +246,27 @@ func (s *fileStore) resource(id string) (io.ReaderAt, int64, func() error, error
|
||||
if entry.data != nil {
|
||||
return bytes.NewReader(entry.data), int64(len(entry.data)), nil, nil
|
||||
}
|
||||
file, err := openReadFile(entry.path)
|
||||
if entry.file != nil && !entry.temporary {
|
||||
info, err := entry.file.Stat()
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
entry.readers++
|
||||
var once sync.Once
|
||||
var closeErr error
|
||||
return entry.file, info.Size(), func() error {
|
||||
once.Do(func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
entry.readers--
|
||||
if entry.retired && entry.readers == 0 {
|
||||
closeErr = entry.file.Close()
|
||||
}
|
||||
})
|
||||
return closeErr
|
||||
}, nil
|
||||
}
|
||||
file, err := basefile.Open(entry.path)
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
@@ -374,9 +403,9 @@ func (s *fileStore) reserve(id, base, extension string, directory bool) (string,
|
||||
}
|
||||
|
||||
// begin 创建文件写入事务
|
||||
// 入参: id 输出授权标识
|
||||
// 入参: ctx 取消上下文, id 输出授权标识
|
||||
// 返回: *fileWriter 事务写入器, error 创建错误
|
||||
func (s *fileStore) begin(id string) (*fileWriter, error) {
|
||||
func (s *fileStore) begin(ctx context.Context, id string) (*fileWriter, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
@@ -386,11 +415,18 @@ func (s *fileStore) begin(id string) (*fileWriter, error) {
|
||||
if entry == nil || entry.directory || entry.path == "" {
|
||||
return nil, fmt.Errorf("输出授权无效")
|
||||
}
|
||||
file, err := temporaryFile(filepath.Dir(entry.path), filepath.Base(entry.path)+".")
|
||||
if entry.saveTarget == nil {
|
||||
var err error
|
||||
entry.saveTarget, err = basefile.NewTarget(entry.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
output, err := entry.saveTarget.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer := &fileWriter{id: rand.Text(), store: s, file: file, target: entry.path}
|
||||
writer := &fileWriter{id: rand.Text(), store: s, output: output}
|
||||
s.writers[writer.id] = writer
|
||||
return writer, nil
|
||||
}
|
||||
@@ -429,7 +465,8 @@ func (s *fileStore) remove(id, name string, recursive bool) error {
|
||||
defer root.Close()
|
||||
for key, entry := range s.entries {
|
||||
if entry.path == target || recursive && strings.HasPrefix(entry.path, target+string(filepath.Separator)) {
|
||||
if entry.file != nil {
|
||||
entry.retired = true
|
||||
if entry.file != nil && entry.readers == 0 {
|
||||
entry.file.Close()
|
||||
}
|
||||
delete(s.entries, key)
|
||||
@@ -445,62 +482,21 @@ func (s *fileStore) remove(id, name string, recursive bool) error {
|
||||
// 入参: data 输出数据
|
||||
// 返回: int 写入字节数, error 写入错误
|
||||
func (w *fileWriter) Write(data []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
n, err := w.file.Write(data)
|
||||
if err == nil && n != len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
w.err = err
|
||||
return n, err
|
||||
return w.output.Write(data)
|
||||
}
|
||||
|
||||
// Close 同步并替换目标文件
|
||||
// 返回: error 提交错误
|
||||
func (w *fileWriter) Close() error {
|
||||
defer w.release()
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return os.ErrClosed
|
||||
}
|
||||
w.closed = true
|
||||
if w.err != nil {
|
||||
return errors.Join(w.err, w.file.Close(), os.Remove(w.file.Name()))
|
||||
}
|
||||
if err := w.file.Sync(); err != nil {
|
||||
w.file.Close()
|
||||
os.Remove(w.file.Name())
|
||||
return err
|
||||
}
|
||||
if err := w.file.Close(); err != nil {
|
||||
os.Remove(w.file.Name())
|
||||
return err
|
||||
}
|
||||
if err := replaceFile(w.file.Name(), w.target); err != nil {
|
||||
os.Remove(w.file.Name())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return w.output.Close()
|
||||
}
|
||||
|
||||
// Abort 删除未提交的临时输出
|
||||
// 返回: error 清理错误
|
||||
func (w *fileWriter) Abort() error {
|
||||
defer w.release()
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
w.closed = true
|
||||
return errors.Join(w.file.Close(), os.Remove(w.file.Name()))
|
||||
return w.output.Abort()
|
||||
}
|
||||
|
||||
// release 移除已结束的写入事务
|
||||
@@ -556,9 +552,12 @@ func (s *fileStore) close() error {
|
||||
if entry.stop != nil {
|
||||
entry.stop()
|
||||
}
|
||||
if entry.file != nil {
|
||||
s.mu.Lock()
|
||||
entry.retired = true
|
||||
if entry.file != nil && entry.readers == 0 {
|
||||
err = errors.Join(err, entry.file.Close())
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return errors.Join(err, os.RemoveAll(s.temporary))
|
||||
}
|
||||
@@ -16,21 +16,9 @@
|
||||
|
||||
package webview
|
||||
|
||||
import "os"
|
||||
|
||||
// conversionMemory 未提供系统内存探测时仅按并行数调度
|
||||
var conversionMemory func() int64
|
||||
|
||||
// openReadFile 打开独立读取句柄
|
||||
func openReadFile(name string) (*os.File, error) {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
// replaceFile 替换目标文件并保留已有读取句柄
|
||||
func replaceFile(source, target string) error {
|
||||
return os.Rename(source, target)
|
||||
}
|
||||
|
||||
// fontNameLanguage 未指定系统显示语言时沿用字体名称表顺序
|
||||
// 返回: uint16 未指定平台标识, uint16 未指定语言标识
|
||||
func fontNameLanguage() (uint16, uint16) {
|
||||
|
||||
@@ -15,68 +15,13 @@
|
||||
package webview
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// replaceFileProc 保留目标文件属性并替换文件内容
|
||||
var replaceFileProc = syscall.NewLazyDLL("kernel32.dll").NewProc("ReplaceFileW")
|
||||
|
||||
// memoryStatusProc 查询系统物理内存和提交额度
|
||||
var memoryStatusProc = syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
|
||||
|
||||
// openReadFile 允许原子替换路径,同时保持已有读取句柄有效
|
||||
func openReadFile(name string) (*os.File, error) {
|
||||
path, err := nativeFilePath(name)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
handle, err := syscall.CreateFile(path, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return os.NewFile(uintptr(handle), name), nil
|
||||
}
|
||||
|
||||
// replaceFile 替换已有文件,目标不存在时直接重命名
|
||||
func replaceFile(source, target string) error {
|
||||
from, err := nativeFilePath(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := nativeFilePath(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, _, err := replaceFileProc.Call(uintptr(unsafe.Pointer(to)), uintptr(unsafe.Pointer(from)), 0, 0, 0, 0)
|
||||
if result != 0 {
|
||||
return nil
|
||||
}
|
||||
if err == syscall.ERROR_FILE_NOT_FOUND {
|
||||
return os.Rename(source, target)
|
||||
}
|
||||
return &os.LinkError{Op: "replace", Old: source, New: target, Err: err}
|
||||
}
|
||||
|
||||
// nativeFilePath 转换绝对路径并支持长路径与网络共享
|
||||
func nativeFilePath(name string) (*uint16, error) {
|
||||
path, err := filepath.Abs(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(path, `\\?\`) {
|
||||
if strings.HasPrefix(path, `\\`) {
|
||||
path = `\\?\UNC\` + path[2:]
|
||||
} else {
|
||||
path = `\\?\` + path
|
||||
}
|
||||
}
|
||||
return syscall.UTF16PtrFromString(path)
|
||||
}
|
||||
|
||||
// fontNameLanguage 获取系统显示语言对应的OpenType名称表标识
|
||||
// 返回: uint16 名称表平台标识, uint16 系统显示语言标识
|
||||
func fontNameLanguage() (uint16, uint16) {
|
||||
|
||||
Reference in new issue
Block a user