-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
258 lines (201 loc) · 5.17 KB
/
main.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"sync"
"time"
)
// main is the function that will be called when program starts, when main function exists program exists
func main() {
// Phase 1 help:
// call parseFlags
// call list
// call merge
// Phase 2 help:
// call watch
// Phase 3 help:
// casting to MergedFile
// https://golang.org/pkg/net/http/#Handle
// https://golang.org/pkg/net/http/#ListenAndServe
// https://golang.org/pkg/sync/#WaitGroup
params := parseFlags()
cssPaths, err := list(params.list)
if err != nil {
log.Fatal(err)
}
err = merge(cssPaths, params.out)
if err != nil {
log.Fatal(err)
}
var wg sync.WaitGroup
if params.watch {
wg.Add(1)
go func() {
err := watch(cssPaths, params.out)
if err != nil {
log.Fatal(err)
}
}()
}
if params.serve != 0 {
wg.Add(1)
go func() {
http.Handle("/", MergedFile(params.out))
fmt.Printf("Serve mode: started on port %d\n", params.serve)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(params.serve), nil))
}()
}
wg.Wait()
}
// cliParams is structure contains cli params flags
type cliParams struct {
list string // phase 1
out string // phase 1
watch bool // phase 2
serve int // phase 3
}
// parseFlags will parse cli program arguments into internal structure for later use
func parseFlags() (params cliParams) {
// Phase 1 help:
// https://golang.org/pkg/flag/#StringVar
flag.StringVar(¶ms.list, "list", "", "Path to dir containg css files")
flag.StringVar(¶ms.out, "out", "", "Filename of destination css file")
// Phase 2 help:
// https://golang.org/pkg/flag/#BoolVar
flag.BoolVar(¶ms.watch, "watch", false, "Enables watch mode that automatically rebuilds destination css file if any of source css files changes")
// Phase 3 help:
// https://golang.org/pkg/flag/#IntVar
flag.IntVar(¶ms.serve, "serve", 0, "Enables serve mode on provided port that will serve merged css file")
// help:
// https://golang.org/pkg/flag/#Parse
flag.Parse()
return
}
// list will read list of css files from list json file
func list(listFile string) (cssFilePaths []string, err error) {
// help:
// https://golang.org/pkg/io/ioutil/#ReadFile
// https://golang.org/pkg/encoding/json/#Unmarshal
content, err := ioutil.ReadFile(listFile)
if err != nil {
return cssFilePaths, err
}
err = json.Unmarshal(content, &cssFilePaths)
if err != nil {
return cssFilePaths, err
}
return
}
// merge will merge css files into one big new file, if merged file exists it will be overwritten
func merge(cssFilePaths []string, mergedFile string) (err error) {
// help:
// https://golang.org/pkg/os/#Create
// https://golang.org/pkg/os/#File.Close
// https://golang.org/pkg/os/#Open
// https://golang.org/pkg/io/#Copy
out, err := os.Create(mergedFile)
if err != nil {
return
}
defer out.Close()
for _, path := range cssFilePaths {
in, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(out, in)
if err != nil {
in.Close()
return err
}
in.Close()
}
return nil
}
// watch will watch changes in cssFilePaths files and rebuild mergedFile
func watch(cssFilePaths []string, mergedFile string) (err error) {
// help: https://golang.org/pkg/os/#Stat
fmt.Println("Watch mode: started")
rebuildCh := make(chan bool)
errCh := make(chan error)
cleanupCh := make(chan struct{})
watchSingleFile := func(path string, rebuildCh chan bool, errCh chan error, cleanupCh chan struct{}) {
initStat, err := os.Stat(path)
if err != nil {
select {
case errCh <- err:
return
case <-cleanupCh:
return
}
}
fmt.Printf("Watch mode: added %q to watch list\n", path)
for {
stat, err := os.Stat(path)
if err != nil {
select {
case errCh <- err:
return
case <-cleanupCh:
return
}
}
if stat.Size() != initStat.Size() || stat.ModTime() != initStat.ModTime() {
select {
case rebuildCh <- true:
initStat = stat
case <-cleanupCh:
return
}
}
time.Sleep(50 * time.Millisecond)
}
}
for _, path := range cssFilePaths {
go watchSingleFile(path, rebuildCh, errCh, cleanupCh)
}
for {
select {
case <-rebuildCh:
err := merge(cssFilePaths, mergedFile)
if err != nil {
close(cleanupCh)
return err
}
fmt.Printf("Watch mode: rebuilded %q\n", mergedFile)
case err := <-errCh:
close(cleanupCh)
return err
}
}
}
// MergedFile is a custom type representing merged css file
type MergedFile string
// ServeHTTP of MergedFile type satisfy http.Handler interface making it accessible via http protocol
func (mf MergedFile) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// help:
// https://golang.org/pkg/os/#Stat
// https://golang.org/pkg/os/#Open
// https://golang.org/pkg/net/http/#ServeContent
// https://golang.org/pkg/net/http/#Error
path := string(mf)
stat, err := os.Stat(path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
out, err := os.Open(path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer out.Close()
http.ServeContent(w, req, stat.Name(), stat.ModTime(), out)
}