]> git.lizzy.rs Git - micro.git/blob - internal/buffer/save.go
Merge pull request #1426 from Nergel3/master
[micro.git] / internal / buffer / save.go
1 package buffer
2
3 import (
4         "bytes"
5         "errors"
6         "io"
7         "os"
8         "os/exec"
9         "os/signal"
10         "path/filepath"
11         "unicode"
12         "unicode/utf8"
13
14         "github.com/zyedidia/micro/internal/config"
15         "github.com/zyedidia/micro/internal/screen"
16         "github.com/zyedidia/micro/internal/util"
17         "golang.org/x/text/encoding"
18         "golang.org/x/text/encoding/htmlindex"
19         "golang.org/x/text/transform"
20 )
21
22 // LargeFileThreshold is the number of bytes when fastdirty is forced
23 // because hashing is too slow
24 const LargeFileThreshold = 50000
25
26 // overwriteFile opens the given file for writing, truncating if one exists, and then calls
27 // the supplied function with the file as io.Writer object, also making sure the file is
28 // closed afterwards.
29 func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error) (err error) {
30         var file *os.File
31
32         if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
33                 return
34         }
35
36         defer func() {
37                 if e := file.Close(); e != nil && err == nil {
38                         err = e
39                 }
40         }()
41
42         w := transform.NewWriter(file, enc.NewEncoder())
43         // w := bufio.NewWriter(file)
44
45         if err = fn(w); err != nil {
46                 return
47         }
48
49         // err = w.Flush()
50         return
51 }
52
53 // overwriteFileAsRoot executes dd as root and then calls the supplied function
54 // with dd's standard input as an io.Writer object. Dd opens the given file for writing,
55 // truncating it if it exists, and writes what it receives on its standard input to the file.
56 func overwriteFileAsRoot(name string, enc encoding.Encoding, fn func(io.Writer) error) (err error) {
57         cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "status=none", "bs=4K", "of="+name)
58         var stdin io.WriteCloser
59
60         screenb := screen.TempFini()
61
62         // This is a trap for Ctrl-C so that it doesn't kill micro
63         // Instead we trap Ctrl-C to kill the program we're running
64         c := make(chan os.Signal, 1)
65         signal.Notify(c, os.Interrupt)
66         go func() {
67                 for range c {
68                         cmd.Process.Kill()
69                 }
70         }()
71
72         if stdin, err = cmd.StdinPipe(); err != nil {
73                 return
74         }
75
76         if err = cmd.Start(); err != nil {
77                 return
78         }
79
80         e := fn(stdin)
81
82         if err = stdin.Close(); err != nil {
83                 return
84         }
85
86         if err = cmd.Wait(); err != nil {
87                 return
88         }
89
90         screen.TempStart(screenb)
91
92         return e
93 }
94
95 // Save saves the buffer to its default path
96 func (b *Buffer) Save() error {
97         return b.SaveAs(b.Path)
98 }
99
100 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
101 func (b *Buffer) SaveAs(filename string) error {
102         return b.saveToFile(filename, false)
103 }
104
105 func (b *Buffer) SaveWithSudo() error {
106         return b.SaveAsWithSudo(b.Path)
107 }
108
109 func (b *Buffer) SaveAsWithSudo(filename string) error {
110         return b.saveToFile(filename, true)
111 }
112
113 func (b *Buffer) saveToFile(filename string, withSudo bool) error {
114         var err error
115         if b.Type.Readonly {
116                 return errors.New("Cannot save readonly buffer")
117         }
118         if b.Type.Scratch {
119                 return errors.New("Cannot save scratch buffer")
120         }
121
122         b.UpdateRules()
123         if b.Settings["rmtrailingws"].(bool) {
124                 for i, l := range b.lines {
125                         leftover := utf8.RuneCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
126
127                         linelen := utf8.RuneCount(l.data)
128                         b.Remove(Loc{leftover, i}, Loc{linelen, i})
129                 }
130
131                 b.RelocateCursors()
132         }
133
134         if b.Settings["eofnewline"].(bool) {
135                 end := b.End()
136                 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
137                         b.Insert(end, "\n")
138                 }
139         }
140
141         // Update the last time this file was updated after saving
142         defer func() {
143                 b.ModTime, _ = util.GetModTime(filename)
144                 err = b.Serialize()
145         }()
146
147         // Removes any tilde and replaces with the absolute path to home
148         absFilename, _ := util.ReplaceHome(filename)
149
150         // Get the leading path to the file | "." is returned if there's no leading path provided
151         if dirname := filepath.Dir(absFilename); dirname != "." {
152                 // Check if the parent dirs don't exist
153                 if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
154                         // Prompt to make sure they want to create the dirs that are missing
155                         if b.Settings["mkparents"].(bool) {
156                                 // Create all leading dir(s) since they don't exist
157                                 if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
158                                         // If there was an error creating the dirs
159                                         return mkdirallErr
160                                 }
161                         } else {
162                                 return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation")
163                         }
164                 }
165         }
166
167         var fileSize int
168
169         enc, err := htmlindex.Get(b.Settings["encoding"].(string))
170         if err != nil {
171                 return err
172         }
173
174         fwriter := func(file io.Writer) (e error) {
175                 if len(b.lines) == 0 {
176                         return
177                 }
178
179                 // end of line
180                 var eol []byte
181                 if b.Endings == FFDos {
182                         eol = []byte{'\r', '\n'}
183                 } else {
184                         eol = []byte{'\n'}
185                 }
186
187                 // write lines
188                 if fileSize, e = file.Write(b.lines[0].data); e != nil {
189                         return
190                 }
191
192                 for _, l := range b.lines[1:] {
193                         if _, e = file.Write(eol); e != nil {
194                                 return
195                         }
196                         if _, e = file.Write(l.data); e != nil {
197                                 return
198                         }
199                         fileSize += len(eol) + len(l.data)
200                 }
201                 return
202         }
203
204         if withSudo {
205                 err = overwriteFileAsRoot(absFilename, enc, fwriter)
206         } else {
207                 err = overwriteFile(absFilename, enc, fwriter)
208         }
209
210         if err != nil {
211                 return err
212         }
213
214         if !b.Settings["fastdirty"].(bool) {
215                 if fileSize > LargeFileThreshold {
216                         // For large files 'fastdirty' needs to be on
217                         b.Settings["fastdirty"] = true
218                 } else {
219                         calcHash(b, &b.origHash)
220                 }
221         }
222
223         b.Path = filename
224         absPath, _ := filepath.Abs(filename)
225         b.AbsPath = absPath
226         b.isModified = false
227         return err
228 }