-
Notifications
You must be signed in to change notification settings - Fork 0
/
file.go
180 lines (166 loc) · 4.03 KB
/
file.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
170
171
172
173
174
175
176
177
178
179
180
package main
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"unicode/utf8"
"github.com/kybin/tor/syntax"
)
// isCreatable check if users has permission to create.
// If the file already exists, it will return error.
//
// NOTE: It actually creates the file then delete.
func isCreatable(f string) (bool, error) {
// ensure the file does not exist.
_, err := os.Stat(f)
if err == nil {
return false, fmt.Errorf("file should not exists: %v", f)
}
if !os.IsNotExist(err) {
return false, err
}
// file creation checking.
file, err := os.Create(f)
if err != nil {
if !os.IsPermission(err) {
return false, err
}
return false, nil
}
if err := file.Close(); err != nil {
return false, err
}
if err := os.Remove(f); err != nil {
// TODO: better finalization for remove failure?
return false, err
}
return true, nil
}
// isWritable checks whether f is writable file or not.
// If it couldn't open the file for check, it will return error.
func isWritable(f string) (bool, error) {
file, err := os.OpenFile(f, os.O_WRONLY, 0666)
if err != nil {
if os.IsPermission(err) {
return false, nil
}
return false, err
}
file.Close()
return true, nil
}
// readOrCreate open f and read it's Text.
// When f doesn't exist and allow to create, it will create a new Text.
func readOrCreate(f string, allowCreate bool) (*Text, error) {
if _, err := os.Stat(f); err != nil {
if os.IsNotExist(err) {
if allowCreate {
return create(f)
}
return nil, errors.New("file not exist. please retry with -new flag.")
}
return nil, err
}
return read(f)
}
// create creates a new Text for f.
// When f is not creatable, it will return error.
func create(f string) (*Text, error) {
writable, err := isCreatable(f)
if err != nil {
return nil, err
}
if !writable {
return nil, errors.New("could not create the file. please check the directory permission.")
}
ext := filepath.Ext(f)
if ext != "" {
ext = strings.TrimPrefix(ext, ".")
}
lang := syntax.NewLanguage(ext)
return &Text{lines: []Line{{""}}, tabToSpace: lang.TabToSpace, tabWidth: lang.TabWidth, writable: writable, lineEnding: "\n"}, nil
}
// read reads a file and returns it as *Text.
// When the file is not exists, it will return error with nil *Text.
func read(f string) (*Text, error) {
file, err := os.Open(f)
if err != nil {
return nil, err
}
defer file.Close()
writable, err := isWritable(f)
if err != nil {
return nil, err
}
// aggregate the text info.
// tor uses tab (4 space) for indentation.
// but when parse an exist file, follow the file's rule.
lines := make([]Line, 0)
tabToSpace := false
tabWidth := 4
findIndentLine := false
scanner := bufio.NewScanner(file)
for scanner.Scan() {
t := scanner.Text()
if !findIndentLine {
r, _ := utf8.DecodeRuneInString(t)
if r == ' ' || r == '\t' {
findIndentLine = true
if r == ' ' {
tabToSpace = true
// calculate tab width
tabWidth = 0
remain := t
for len(remain) != 0 {
r, rlen := utf8.DecodeRuneInString(remain)
remain = remain[rlen:]
if r != ' ' {
break
}
tabWidth++
}
}
}
}
lines = append(lines, Line{t})
}
if err := scanner.Err(); err != nil {
return nil, err
}
// check line ending
lineEnding := "\n"
if len(lines) != 0 {
file.Seek(0, 0)
reader := bufio.NewReader(file)
firstLine, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return nil, err
}
if len(firstLine) >= 2 && firstLine[len(firstLine)-2:] == "\r\n" {
lineEnding = "\r\n"
}
}
// `touch` cmd creates a file with no content.
// avoid program panic from empty text.
if len(lines) == 0 {
lines = []Line{{""}}
}
return &Text{lines: lines, tabToSpace: tabToSpace, tabWidth: tabWidth, writable: writable, lineEnding: lineEnding}, nil
}
// save saves Text to a file.
func save(f string, t *Text) error {
file, err := os.Create(f)
if err != nil {
return err
}
defer file.Close()
for _, line := range t.lines {
file.WriteString(line.data)
file.WriteString(t.lineEnding)
}
return nil
}