forked from DavidGamba/dgtools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
fsmodtime.go
169 lines (153 loc) · 4.37 KB
/
fsmodtime.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// This file is part of fsmodtime.
//
// Copyright (C) 2021 David Gamba Rios
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// package fsmodtime provides functions to compare fs mod times.
//
// The goal of this package is to have functions that allow me to determine if
// my sources (fs dependencies) have been modified (changed) so that based on
// that I can rebuild my targets.
//
// Additionally I want to log the file (not necessarily all of them but at least one) that
// changed for informational/verification purposes when building build systems downstream.
//
// The function that meets that goal is [Target].
//
// Requirements:
//
// * Targets might not exist yet.
// In that case, build them.
// * All declared targets must exist.
// Rebuild otherwise.
// * Not all sources are required to exist.
// For example, I might want a blank *.jpg and *.png in my build system but some image types might not exist.
// * ExpandEnv but don't fail silently if my Env Var expansions fail.
// * Allow for globs.
//
// Example:
//
// targets := []string{"$outputs_dir/doc.pdf", "$outputs_dir/*.html"}
// sources := []string{"$src_dir/*.adoc", "$images_dir/*.jpg", "$images_dir/*.png"}
// paths, modified, err := Target(os.DirFS("."), targets, sources)
package fsmodtime
import (
"fmt"
"io/fs"
"io/ioutil"
"log"
"path"
"path/filepath"
)
var Logger = log.New(ioutil.Discard, "", log.LstdFlags)
var ErrorInvalidPath = fmt.Errorf("invalid path")
var ErrorInvalidFS = fmt.Errorf("invalid fs")
// Last - given a list of paths, it finds the file with the latest modTime and returns it.
//
// root := "."
// fileSystem := os.DirFS(root)
// path, fi, err := Last(fileSystem, paths)
func Last(fsys fs.FS, paths []string) (filepath string, fileInfo fs.FileInfo, err error) {
// TODO: Add option to skip descending into dirs.
// TODO: Add option to follow symlinks.
// TODO: Add variadic option definitions.
afterFn := func(root string, fi fs.FileInfo) error {
Logger.Printf("fn: %s\n", path.Join(root, fi.Name()))
if fileInfo == nil {
fileInfo = fi
filepath = root
return nil
}
if fi.ModTime().After(fileInfo.ModTime()) {
fileInfo = fi
filepath = root
}
return nil
}
err = walkPaths(fsys, paths, afterFn)
if err != nil {
return "", nil, err
}
return filepath, fileInfo, nil
}
// First - given a list of paths, it finds the file with the earliest modTime and returns it.
//
// root := "."
// fileSystem := os.DirFS(root)
// path, fi, err := First(fileSystem, paths)
func First(fsys fs.FS, paths []string) (filepath string, fileInfo fs.FileInfo, err error) {
beforeFn := func(root string, fi fs.FileInfo) error {
Logger.Printf("fn: %s\n", path.Join(root, fi.Name()))
if fileInfo == nil {
fileInfo = fi
filepath = root
return nil
}
if fi.ModTime().Before(fileInfo.ModTime()) {
fileInfo = fi
filepath = root
}
return nil
}
err = walkPaths(fsys, paths, beforeFn)
if err != nil {
return "", nil, err
}
return filepath, fileInfo, nil
}
func walkPaths(fsys fs.FS, paths []string, fn fileInfoFn) error {
if fsys == nil {
return ErrorInvalidFS
}
for _, path := range paths {
Logger.Printf("path: %s\n", path)
// validate path
if path == "" {
return fmt.Errorf("%w: '%s'", ErrorInvalidPath, path)
}
fi, err := fs.Stat(fsys, path)
if err != nil {
return err
}
err = fileInfoIterate(fsys, filepath.Dir(path), fs.FileInfoToDirEntry(fi), fn)
if err != nil {
return err
}
}
return nil
}
type fileInfoFn func(root string, fi fs.FileInfo) error
// Given a fs.DirEntry (or a fs.FileInfo using `fs.FileInfoToDirEntry(fi)`) it
// expands every dir and runs fn on every resulting child fs.DirEntry.
//
// NOTE: It doesn't run fn on dirs.
//
// TODO: It doesn't follow symlinks
func fileInfoIterate(fsys fs.FS, root string, de fs.DirEntry, fn fileInfoFn) (err error) {
if de.IsDir() {
dir := path.Join(root, de.Name())
Logger.Printf("expand: %s\n", dir)
dirEntries, err := fs.ReadDir(fsys, dir)
if err != nil {
return err
}
for _, de := range dirEntries {
err := fileInfoIterate(fsys, dir, de, fn)
if err != nil {
return err
}
}
return nil
}
fi, err := de.Info()
if err != nil {
return err
}
err = fn(root, fi)
if err != nil {
return err
}
return
}