156 lines
4.5 KiB
Go
156 lines
4.5 KiB
Go
package runtimelog
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
)
|
||
|
||
const legacyDirectory = "运行日志"
|
||
|
||
// migrateLegacyLogs 在日志写入和维护启动前迁移旧目录;不覆盖同名文件,不处理未知文件。
|
||
func migrateLegacyLogs(root *os.Root) error {
|
||
info, err := root.Lstat(legacyDirectory)
|
||
if os.IsNotExist(err) {
|
||
return nil
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||
return fmt.Errorf("旧运行日志目录必须是实际目录:%w", ErrInvalid)
|
||
}
|
||
legacy, err := root.OpenRoot(legacyDirectory)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer legacy.Close()
|
||
directory, err := legacy.Open(".")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
opened, statErr := directory.Stat()
|
||
entries, readErr := directory.ReadDir(-1)
|
||
if err := errors.Join(statErr, readErr, directory.Close()); err != nil {
|
||
return err
|
||
}
|
||
if !os.SameFile(info, opened) {
|
||
return fmt.Errorf("旧运行日志目录在迁移前发生变化:%w", ErrInvalid)
|
||
}
|
||
type sourceFile struct {
|
||
name string
|
||
info os.FileInfo
|
||
}
|
||
var sources []sourceFile
|
||
unknown := false
|
||
// 先检查全部源文件和目标,不能迁移到一半才发现已有同名日志。
|
||
for _, entry := range entries {
|
||
name := entry.Name()
|
||
if _, ok := fileDay(name); !ok {
|
||
unknown = true
|
||
continue
|
||
}
|
||
source, err := legacy.Lstat(name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !source.Mode().IsRegular() {
|
||
return fmt.Errorf("旧运行日志不是普通文件 %s:%w", name, ErrInvalid)
|
||
}
|
||
if _, err := root.Lstat(name); err == nil {
|
||
return fmt.Errorf("运行日志迁移目标已存在,请核对两个目录的同名文件,拒绝覆盖:%s", name)
|
||
} else if !os.IsNotExist(err) {
|
||
return err
|
||
}
|
||
sources = append(sources, sourceFile{name: name, info: source})
|
||
}
|
||
for _, source := range sources {
|
||
if err := moveLegacyLog(legacy, root, source.name, source.info); err != nil {
|
||
return fmt.Errorf("运行日志迁移失败,未完成的源文件已保留 %s:%w", source.name, err)
|
||
}
|
||
}
|
||
if unknown {
|
||
return nil
|
||
}
|
||
if err := legacy.Close(); err != nil {
|
||
return err
|
||
}
|
||
current, err := root.Lstat(legacyDirectory)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !os.SameFile(info, current) || !current.IsDir() || current.Mode()&os.ModeSymlink != 0 {
|
||
return fmt.Errorf("旧运行日志目录在迁移时发生变化:%w", ErrInvalid)
|
||
}
|
||
// 只移除迁移后的空目录;新出现的文件会使移除失败,绝不递归删除。
|
||
return root.Remove(legacyDirectory)
|
||
}
|
||
|
||
// moveLegacyLog 使用排他创建和流式复制,兼容不支持硬链接的文件系统。
|
||
func moveLegacyLog(legacy, root *os.Root, name string, expected os.FileInfo) error {
|
||
source, err := legacy.Open(name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer source.Close()
|
||
opened, err := source.Stat()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !unchangedLog(expected, opened) {
|
||
return ErrInvalid
|
||
}
|
||
target, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
created, err := target.Stat()
|
||
complete := false
|
||
defer func() {
|
||
_ = target.Close()
|
||
// 只清理本次创建且尚未复制完成的目标;中断或异常时源文件仍然存在。
|
||
if !complete && created != nil {
|
||
if current, err := root.Lstat(name); err == nil && os.SameFile(created, current) {
|
||
_ = root.Remove(name)
|
||
}
|
||
}
|
||
}()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
written, copyErr := io.Copy(target, source)
|
||
after, statErr := source.Stat()
|
||
if err := errors.Join(copyErr, statErr, target.Sync(), target.Close(), source.Close()); err != nil {
|
||
return err
|
||
}
|
||
if written != expected.Size() || !unchangedLog(expected, after) {
|
||
return fmt.Errorf("源日志在复制时发生变化:%w", ErrInvalid)
|
||
}
|
||
if err := root.Chtimes(name, expected.ModTime(), expected.ModTime()); err != nil {
|
||
return err
|
||
}
|
||
current, err := legacy.Lstat(name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !unchangedLog(expected, current) {
|
||
return fmt.Errorf("源日志在迁移时发生变化:%w", ErrInvalid)
|
||
}
|
||
destination, err := root.Lstat(name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !destination.Mode().IsRegular() || !os.SameFile(created, destination) || destination.Size() != written {
|
||
return fmt.Errorf("目标日志在迁移时发生变化:%w", ErrInvalid)
|
||
}
|
||
// 目标已完整落盘;删除源失败时保留两份,重启明确报冲突,不猜测应保留哪份。
|
||
complete = true
|
||
return legacy.Remove(name)
|
||
}
|
||
|
||
func unchangedLog(expected, current os.FileInfo) bool {
|
||
return current.Mode().IsRegular() && os.SameFile(expected, current) &&
|
||
expected.Size() == current.Size() && expected.ModTime().Equal(current.ModTime())
|
||
}
|