Files
ofdgo/internal/base/file/file.go
T
2026-10-18 23:46:02 +08:00

217 lines
5.2 KiB
Go

// 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()))
}