|
1 | 1 | package installer |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "fmt" |
| 5 | + "io" |
4 | 6 | "os" |
| 7 | + "path" |
5 | 8 | "sync" |
6 | 9 |
|
7 | 10 | "go.evanpurkhiser.com/dots/config" |
8 | | - "go.evanpurkhiser.com/dots/resolver" |
9 | 11 | ) |
10 | 12 |
|
11 | 13 | const separator = string(os.PathSeparator) |
12 | 14 |
|
13 | | -// A PreparedDotfile represents a dotfile that has been "prepared" for |
14 | | -// installation by verifying it's contents against the existing dotfile, and |
15 | | -// checking various other flags that require knowledge of the existing dotfile. |
16 | | -type PreparedDotfile struct { |
17 | | - *resolver.Dotfile |
| 15 | +// directoryMode is the mode used to create directories for installed dotfiles. |
| 16 | +const directoryMode = 0755 |
18 | 17 |
|
19 | | - // InstallPath is the full path to which the dotfile will be installed. |
20 | | - InstallPath string |
| 18 | +// InstallConfig represents configuration options available for installing |
| 19 | +// a single or set of dotfiles. |
| 20 | +type InstallConfig struct { |
| 21 | + SourceConfig *config.SourceConfig |
21 | 22 |
|
22 | | - // ContentsDiffer is a boolean flag representing that the compiled source |
23 | | - // differs from the currently installed dotfile. |
24 | | - ContentsDiffer bool |
25 | | - |
26 | | - // SourceModesDiffer indicates that a compiled dotfile (one with multiple |
27 | | - // sources) does not have a consistent mode across all sources. In this |
28 | | - // case the lowest mode will be used. |
29 | | - SourceModesDiffer bool |
30 | | - |
31 | | - // ModeDiffers represents the change in modes between the compiled source |
32 | | - // and the currently installed dotfile. Equal modes can be verified by |
33 | | - // calling ModeDiff.IsSame. |
34 | | - ModeDiff ModeDiff |
35 | | - |
36 | | - // RemovedNull is a warning flag indicating that the removed dotfile does |
37 | | - // not exist in the install tree, though the dotfile is marked as removed. |
38 | | - RemovedNull bool |
39 | | - |
40 | | - // OverwritesExisting is a warning flag that indicates that installing this |
41 | | - // dotfile is overwriting a dotfile that was not part of the lockfile. |
42 | | - OverwritesExisting bool |
43 | | - |
44 | | - // PrepareError keeps track of errors while preparing the dotfile. Should |
45 | | - // this contain any errors, the PreparedDotfile is likely incomplete. |
46 | | - PrepareError error |
47 | | -} |
48 | | - |
49 | | -// ModeDiff represents a change in file mode. |
50 | | -type ModeDiff struct { |
51 | | - Old os.FileMode |
52 | | - New os.FileMode |
| 23 | + // OverrideInstallPath specifies a path to install the dotfile at, |
| 24 | + // overriding the configuration in the SourceConfig. |
| 25 | + OverrideInstallPath string |
53 | 26 | } |
54 | 27 |
|
55 | | -// IsSame returns a boolean value indicating if the modes are equal. |
56 | | -func (d ModeDiff) IsSame() bool { |
57 | | - return d.New == d.Old |
58 | | -} |
| 28 | +func InstallDotfile(dotfile *PreparedDotfile, config InstallConfig) error { |
| 29 | + installPath := config.SourceConfig.InstallPath + separator + dotfile.Path |
59 | 30 |
|
60 | | -// PreparedDotfiles is a list of prepared dotfiles. |
61 | | -type PreparedDotfiles []*PreparedDotfile |
| 31 | + if config.OverrideInstallPath != "" { |
| 32 | + installPath = config.OverrideInstallPath + separator + dotfile.Path |
| 33 | + } |
62 | 34 |
|
63 | | -// PrepareDotfiles iterates all passed dotfiles and creates an associated |
64 | | -// PreparedDotfile, returning a list of all prepared dotfiles. |
65 | | -func PrepareDotfiles(dotfiles resolver.Dotfiles, config config.SourceConfig) PreparedDotfiles { |
66 | | - preparedDotfiles := make(PreparedDotfiles, len(dotfiles)) |
| 35 | + if dotfile.SourcesAreIrregular { |
| 36 | + return fmt.Errorf("Source files are not all regular files") |
| 37 | + } |
67 | 38 |
|
68 | | - waitGroup := sync.WaitGroup{} |
69 | | - waitGroup.Add(len(dotfiles)) |
| 39 | + // No change |
| 40 | + if !dotfile.IsNew && !dotfile.ContentsDiffer && dotfile.Permissions.IsSame() { |
| 41 | + return nil |
| 42 | + } |
70 | 43 |
|
71 | | - prepare := func(index int, dotfile *resolver.Dotfile) { |
72 | | - defer waitGroup.Done() |
| 44 | + // Removed |
| 45 | + if dotfile.Removed && !dotfile.RemovedNull { |
| 46 | + return os.Remove(installPath) |
| 47 | + } |
73 | 48 |
|
74 | | - installPath := config.InstallPath + separator + dotfile.Path |
| 49 | + targetMode := dotfile.Permissions.New | dotfile.Mode |
75 | 50 |
|
76 | | - prepared := PreparedDotfile{ |
77 | | - Dotfile: dotfile, |
78 | | - InstallPath: installPath, |
79 | | - } |
80 | | - preparedDotfiles[index] = &prepared |
| 51 | + // Only permissions differ |
| 52 | + if !dotfile.IsNew && !dotfile.Permissions.IsSame() && !dotfile.ContentsDiffer { |
| 53 | + return os.Chmod(installPath, targetMode) |
| 54 | + } |
81 | 55 |
|
82 | | - targetStat, targetStatErr := os.Lstat(installPath) |
| 56 | + if err := os.MkdirAll(path.Dir(installPath), directoryMode); err != nil { |
| 57 | + return err |
| 58 | + } |
83 | 59 |
|
84 | | - exists := !os.IsNotExist(targetStatErr) |
| 60 | + targetOpts := os.O_CREATE | os.O_TRUNC | os.O_WRONLY |
85 | 61 |
|
86 | | - // If we're unable to stat our target installation file and the file |
87 | | - // exists there's likely a permissions issue. |
88 | | - if targetStatErr != nil && exists { |
89 | | - prepared.PrepareError = targetStatErr |
90 | | - return |
91 | | - } |
| 62 | + target, err := os.OpenFile(installPath, targetOpts, targetMode) |
| 63 | + if err != nil { |
| 64 | + return err |
| 65 | + } |
| 66 | + defer target.Close() |
92 | 67 |
|
93 | | - // Nothing needs to be verified if the dotfile is simply being added |
94 | | - if dotfile.Added && !exists { |
95 | | - return |
96 | | - } |
| 68 | + source, err := OpenDotfile(dotfile.Dotfile, *config.SourceConfig) |
| 69 | + if err != nil { |
| 70 | + return err |
| 71 | + } |
| 72 | + defer source.Close() |
97 | 73 |
|
98 | | - if dotfile.Added && exists { |
99 | | - prepared.OverwritesExisting = true |
100 | | - } |
| 74 | + _, err = io.Copy(target, source) |
101 | 75 |
|
102 | | - if dotfile.Removed && !exists { |
103 | | - prepared.RemovedNull = true |
104 | | - } |
| 76 | + return err |
| 77 | +} |
105 | 78 |
|
106 | | - sourceInfo := make([]os.FileInfo, len(dotfile.Sources)) |
| 79 | +func InstallDotfiles(dotfiles PreparedDotfiles, config InstallConfig) []error { |
| 80 | + waitGroup := sync.WaitGroup{} |
| 81 | + waitGroup.Add(len(dotfiles)) |
107 | 82 |
|
108 | | - for i, source := range dotfile.Sources { |
109 | | - path := config.SourcePath + separator + source.Path |
| 83 | + errors := []error{} |
110 | 84 |
|
111 | | - info, err := os.Lstat(path) |
| 85 | + for _, dotfile := range dotfiles { |
| 86 | + go func(dotfile *PreparedDotfile) { |
| 87 | + err := InstallDotfile(dotfile, config) |
112 | 88 | if err != nil { |
113 | | - prepared.PrepareError = err |
114 | | - return |
| 89 | + errors = append(errors, err) |
115 | 90 | } |
116 | | - sourceInfo[i] = info |
117 | | - } |
118 | | - |
119 | | - sourceMode, tookLowest := flattenModes(sourceInfo) |
120 | | - |
121 | | - prepared.ModeDiff = ModeDiff{ |
122 | | - Old: targetStat.Mode(), |
123 | | - New: sourceMode, |
124 | | - } |
125 | | - prepared.SourceModesDiffer = tookLowest |
126 | | - |
127 | | - // If we are dealing with a dotfile with a single source we can quickly |
128 | | - // determine modification based on differing sizes, otherwise we will |
129 | | - // have to compare the compiled sources to the installed file. |
130 | | - if len(dotfile.Sources) == 1 && targetStat.Size() != sourceInfo[0].Size() { |
131 | | - prepared.ContentsDiffer = true |
132 | | - return |
133 | | - } |
134 | | - |
135 | | - // Compare source and currently instlled dotfile |
136 | | - source, err := OpenDotfile(dotfile, config) |
137 | | - if err != nil { |
138 | | - prepared.PrepareError = err |
139 | | - return |
140 | | - } |
141 | | - defer source.Close() |
142 | | - |
143 | | - target, err := os.Open(installPath) |
144 | | - if err != nil { |
145 | | - prepared.PrepareError = err |
146 | | - return |
147 | | - } |
148 | | - defer target.Close() |
149 | | - |
150 | | - filesAreSame, err := compareReaders(source, target) |
151 | | - if err != nil { |
152 | | - prepared.PrepareError = err |
153 | | - } |
154 | | - |
155 | | - prepared.ContentsDiffer = !filesAreSame |
156 | | - } |
157 | | - |
158 | | - for i, dotfile := range dotfiles { |
159 | | - go prepare(i, dotfile) |
| 91 | + waitGroup.Done() |
| 92 | + }(dotfile) |
160 | 93 | } |
161 | 94 |
|
162 | 95 | waitGroup.Wait() |
163 | 96 |
|
164 | | - return preparedDotfiles |
| 97 | + return errors |
165 | 98 | } |
0 commit comments