Initial dirty but working commit
This commit is contained in:
parent
045b274107
commit
28b18ec679
7 changed files with 1927 additions and 0 deletions
13
README.md
Normal file
13
README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# recipe-card
|
||||
WIP (but fully functional) and extremely messy at the moment.
|
||||
|
||||
## Quick Start
|
||||
Use the Google Docs recipe template to for your recipes.
|
||||
|
||||
Have scans of jpegs by your docx files, and have your docx/images in folders specific to that recipe.
|
||||
|
||||
One docx file and as many jpeg images as you want. The image in the recipe tempalte will also be used.
|
||||
|
||||
Just `go get github.com/tblyler/recipe-card` and run `recipe-card`.
|
||||
|
||||
Run with `--help` for options.
|
131
doc/docx.go
Normal file
131
doc/docx.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package doc
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// xmlFileName is the one true XML file in a docx file that has
|
||||
// the textual information we desire
|
||||
xmlFileName = "word/document.xml"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingDocument happens when xmlFileName is missing from zip
|
||||
ErrMissingDocument = fmt.Errorf("Unable to find %s in docx", xmlFileName)
|
||||
)
|
||||
|
||||
// Docx parses docx-formated readers
|
||||
// this is go routine safe
|
||||
type Docx struct {
|
||||
xmlData []byte
|
||||
Image []byte
|
||||
}
|
||||
|
||||
// NewDocx creates a new Docx instance with data from the given reader
|
||||
func NewDocx(reader io.ReaderAt, size int64) (doc *Docx, err error) {
|
||||
doc = new(Docx)
|
||||
|
||||
// docx files are just zip'd xml documents
|
||||
zipReader, err := zip.NewReader(reader, size)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// find the xmlFileName file in the zip
|
||||
var fileReader io.ReadCloser
|
||||
for _, file := range zipReader.File {
|
||||
if doc.xmlData != nil && doc.Image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lowerFileName := strings.ToLower(file.Name)
|
||||
if doc.Image == nil && (strings.HasSuffix(lowerFileName, ".jpg") || strings.HasSuffix(lowerFileName, ".jpeg")) {
|
||||
fileReader, err = file.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
defer fileReader.Close()
|
||||
|
||||
doc.Image, err = ioutil.ReadAll(fileReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if doc.xmlData == nil && lowerFileName == xmlFileName {
|
||||
// open xmlFileName for extraction
|
||||
fileReader, err = file.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer fileReader.Close()
|
||||
|
||||
// store all extracted XML data to doc.xmlData
|
||||
doc.xmlData, err = ioutil.ReadAll(fileReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doc.xmlData != nil && doc.Image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil, ErrMissingDocument
|
||||
}
|
||||
|
||||
// Text returns each line of (unformatted) text from the docx xml
|
||||
func (d *Docx) Text() (lines []string, err error) {
|
||||
// create an XML decoder for the raw xml data
|
||||
decoder := xml.NewDecoder(bytes.NewReader(d.xmlData))
|
||||
|
||||
// determines if xml.CharData tokens should start to be added to the
|
||||
// lines slice
|
||||
outputCharData := false
|
||||
|
||||
var token xml.Token
|
||||
for {
|
||||
// get the current xml token
|
||||
token, err = decoder.Token()
|
||||
if err != nil {
|
||||
// end of file reached, reset err to nil
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch t := token.(type) {
|
||||
case xml.StartElement:
|
||||
// only start outputing chardata xml tokens if we started to look at
|
||||
// the "body" of the xml document
|
||||
if !outputCharData && strings.ToLower(t.Name.Local) == "body" {
|
||||
outputCharData = true
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case xml.CharData:
|
||||
if outputCharData {
|
||||
// cast to string and get rid of unneeded whitespace
|
||||
str := strings.TrimSpace(string(t))
|
||||
|
||||
// only add lines that actually have data
|
||||
if str != "" {
|
||||
lines = append(lines, str)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
610
doc/docx_test.go
Normal file
610
doc/docx_test.go
Normal file
|
@ -0,0 +1,610 @@
|
|||
package doc
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testDocx = []byte{80, 75, 3, 4, 20, 0, 8, 8, 8, 0, 7, 140,
|
||||
222, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 11, 0, 0, 0, 95, 114, 101, 108, 115, 47,
|
||||
46, 114, 101, 108, 115, 173, 146, 77, 75, 3, 65, 12,
|
||||
134, 239, 253, 21, 67, 238, 221, 108, 43, 136, 200, 206,
|
||||
246, 34, 66, 111, 34, 245, 7, 132, 153, 236, 238, 208,
|
||||
206, 7, 51, 105, 173, 255, 222, 65, 10, 186, 80, 138,
|
||||
160, 199, 188, 121, 243, 240, 28, 210, 109, 206, 254, 160,
|
||||
78, 156, 139, 139, 65, 195, 170, 105, 65, 113, 48, 209,
|
||||
186, 48, 106, 120, 219, 61, 47, 31, 96, 211, 47, 186,
|
||||
87, 62, 144, 212, 74, 153, 92, 42, 170, 222, 132, 162,
|
||||
97, 18, 73, 143, 136, 197, 76, 236, 169, 52, 49, 113,
|
||||
168, 155, 33, 102, 79, 82, 199, 60, 98, 34, 179, 167,
|
||||
145, 113, 221, 182, 247, 152, 127, 50, 160, 159, 49, 213,
|
||||
214, 106, 200, 91, 187, 2, 181, 251, 72, 252, 55, 54,
|
||||
122, 22, 178, 36, 132, 38, 102, 94, 166, 92, 175, 179,
|
||||
56, 46, 21, 78, 121, 100, 209, 96, 163, 121, 169, 113,
|
||||
249, 106, 52, 149, 12, 120, 93, 104, 253, 123, 161, 56,
|
||||
12, 206, 240, 83, 52, 71, 207, 65, 174, 121, 241, 89,
|
||||
56, 88, 182, 183, 149, 40, 165, 91, 70, 119, 255, 105,
|
||||
52, 111, 124, 203, 188, 199, 108, 209, 94, 226, 139, 205,
|
||||
162, 195, 217, 27, 244, 159, 80, 75, 7, 8, 232, 208,
|
||||
1, 35, 217, 0, 0, 0, 61, 2, 0, 0, 80, 75,
|
||||
3, 4, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
16, 0, 0, 0, 100, 111, 99, 80, 114, 111, 112, 115,
|
||||
47, 97, 112, 112, 46, 120, 109, 108, 157, 145, 203, 110,
|
||||
194, 48, 16, 69, 247, 253, 138, 200, 98, 75, 156, 240,
|
||||
42, 69, 142, 81, 31, 234, 10, 169, 72, 77, 75, 119,
|
||||
200, 181, 39, 137, 171, 196, 182, 236, 1, 193, 223, 215,
|
||||
20, 41, 100, 221, 221, 189, 115, 71, 103, 198, 99, 182,
|
||||
62, 117, 109, 114, 4, 31, 180, 53, 5, 201, 211, 140,
|
||||
36, 96, 164, 85, 218, 212, 5, 249, 40, 95, 199, 75,
|
||||
146, 4, 20, 70, 137, 214, 26, 40, 200, 25, 2, 89,
|
||||
243, 59, 182, 245, 214, 129, 71, 13, 33, 137, 4, 19,
|
||||
10, 210, 32, 186, 21, 165, 65, 54, 208, 137, 144, 198,
|
||||
216, 196, 164, 178, 190, 19, 24, 173, 175, 169, 173, 42,
|
||||
45, 225, 197, 202, 67, 7, 6, 233, 36, 203, 22, 20,
|
||||
78, 8, 70, 129, 26, 187, 30, 72, 174, 196, 213, 17,
|
||||
255, 11, 85, 86, 94, 246, 11, 159, 229, 217, 69, 30,
|
||||
103, 37, 116, 174, 21, 8, 156, 209, 155, 44, 45, 138,
|
||||
182, 212, 29, 240, 44, 150, 123, 195, 30, 157, 107, 181,
|
||||
20, 24, 47, 194, 55, 250, 219, 195, 219, 223, 8, 58,
|
||||
79, 167, 233, 44, 205, 71, 27, 109, 14, 167, 253, 215,
|
||||
114, 177, 95, 204, 146, 65, 195, 62, 62, 225, 7, 36,
|
||||
210, 105, 54, 122, 58, 232, 86, 141, 115, 70, 135, 48,
|
||||
182, 21, 53, 4, 30, 171, 87, 193, 118, 214, 171, 192,
|
||||
39, 115, 70, 175, 138, 61, 55, 194, 11, 137, 241, 59,
|
||||
248, 195, 148, 209, 129, 29, 68, 59, 141, 205, 187, 19,
|
||||
242, 130, 202, 150, 195, 174, 65, 18, 103, 121, 81, 123,
|
||||
225, 154, 192, 239, 47, 3, 123, 23, 77, 127, 106, 254,
|
||||
11, 80, 75, 7, 8, 126, 47, 253, 199, 37, 1, 0,
|
||||
0, 0, 2, 0, 0, 80, 75, 3, 4, 20, 0, 8,
|
||||
8, 8, 0, 7, 140, 222, 74, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 100,
|
||||
111, 99, 80, 114, 111, 112, 115, 47, 99, 111, 114, 101,
|
||||
46, 120, 109, 108, 109, 82, 91, 79, 194, 48, 20, 126,
|
||||
247, 87, 44, 125, 223, 186, 139, 65, 211, 108, 35, 81,
|
||||
195, 147, 36, 38, 64, 52, 190, 213, 238, 48, 170, 107,
|
||||
215, 180, 7, 6, 255, 222, 110, 192, 132, 200, 219, 249,
|
||||
46, 253, 78, 207, 105, 243, 233, 94, 53, 193, 14, 172,
|
||||
147, 173, 46, 72, 18, 197, 36, 0, 45, 218, 74, 234,
|
||||
186, 32, 171, 229, 44, 124, 36, 129, 67, 174, 43, 222,
|
||||
180, 26, 10, 114, 0, 71, 166, 229, 93, 46, 12, 19,
|
||||
173, 133, 55, 219, 26, 176, 40, 193, 5, 62, 72, 59,
|
||||
38, 76, 65, 54, 136, 134, 81, 234, 196, 6, 20, 119,
|
||||
145, 119, 104, 47, 174, 91, 171, 56, 122, 104, 107, 106,
|
||||
184, 248, 225, 53, 208, 52, 142, 39, 84, 1, 242, 138,
|
||||
35, 167, 125, 96, 104, 198, 68, 114, 138, 172, 196, 24,
|
||||
105, 182, 182, 25, 2, 42, 65, 161, 1, 5, 26, 29,
|
||||
77, 162, 132, 254, 121, 17, 172, 114, 55, 15, 12, 202,
|
||||
133, 83, 73, 60, 24, 184, 105, 61, 139, 163, 123, 239,
|
||||
228, 104, 236, 186, 46, 234, 178, 193, 234, 239, 159, 208,
|
||||
143, 249, 235, 98, 24, 53, 148, 186, 95, 149, 0, 82,
|
||||
230, 149, 96, 194, 2, 199, 214, 150, 57, 189, 4, 190,
|
||||
174, 192, 9, 43, 13, 250, 149, 31, 197, 43, 194, 227,
|
||||
134, 235, 122, 235, 247, 83, 130, 14, 87, 139, 193, 50,
|
||||
82, 253, 230, 27, 238, 112, 238, 223, 104, 45, 161, 122,
|
||||
58, 248, 140, 27, 220, 105, 19, 76, 157, 184, 192, 143,
|
||||
192, 142, 3, 159, 165, 247, 236, 249, 101, 57, 35, 101,
|
||||
26, 39, 15, 97, 60, 9, 179, 120, 153, 100, 44, 75,
|
||||
89, 114, 255, 217, 55, 189, 14, 24, 58, 91, 216, 201,
|
||||
254, 171, 148, 233, 208, 116, 132, 253, 173, 221, 246, 235,
|
||||
27, 4, 30, 71, 26, 129, 175, 81, 98, 3, 71, 250,
|
||||
92, 254, 251, 62, 229, 47, 80, 75, 7, 8, 45, 164,
|
||||
180, 116, 82, 1, 0, 0, 138, 2, 0, 0, 80, 75,
|
||||
3, 4, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
28, 0, 0, 0, 119, 111, 114, 100, 47, 95, 114, 101,
|
||||
108, 115, 47, 100, 111, 99, 117, 109, 101, 110, 116, 46,
|
||||
120, 109, 108, 46, 114, 101, 108, 115, 173, 146, 203, 106,
|
||||
195, 48, 16, 69, 247, 249, 10, 49, 251, 90, 118, 250,
|
||||
160, 20, 203, 217, 148, 66, 182, 197, 253, 0, 69, 30,
|
||||
63, 136, 245, 64, 154, 148, 230, 239, 59, 52, 33, 113,
|
||||
32, 152, 46, 188, 188, 87, 154, 59, 71, 51, 42, 55,
|
||||
63, 118, 20, 223, 24, 211, 224, 157, 130, 34, 203, 65,
|
||||
160, 51, 190, 25, 92, 167, 224, 171, 254, 120, 120, 133,
|
||||
77, 181, 42, 63, 113, 212, 196, 87, 82, 63, 132, 36,
|
||||
184, 198, 37, 5, 61, 81, 120, 147, 50, 153, 30, 173,
|
||||
78, 153, 15, 232, 248, 164, 245, 209, 106, 98, 25, 59,
|
||||
25, 180, 217, 235, 14, 229, 58, 207, 95, 100, 156, 102,
|
||||
64, 117, 147, 41, 182, 141, 130, 184, 109, 10, 16, 245,
|
||||
49, 224, 127, 178, 125, 219, 14, 6, 223, 189, 57, 88,
|
||||
116, 116, 167, 133, 76, 116, 28, 49, 113, 162, 142, 29,
|
||||
146, 130, 147, 206, 56, 7, 228, 253, 246, 235, 37, 219,
|
||||
187, 131, 221, 97, 228, 65, 94, 9, 46, 214, 28, 196,
|
||||
227, 146, 16, 173, 119, 84, 235, 221, 136, 87, 136, 139,
|
||||
53, 7, 241, 180, 232, 34, 144, 136, 31, 61, 93, 197,
|
||||
217, 153, 67, 120, 94, 18, 129, 184, 118, 50, 131, 63,
|
||||
121, 50, 139, 51, 195, 170, 148, 55, 191, 188, 250, 5,
|
||||
80, 75, 7, 8, 41, 11, 4, 96, 232, 0, 0, 0,
|
||||
28, 3, 0, 0, 80, 75, 3, 4, 20, 0, 8, 8,
|
||||
8, 0, 7, 140, 222, 74, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 119, 111,
|
||||
114, 100, 47, 115, 116, 121, 108, 101, 115, 46, 120, 109,
|
||||
108, 205, 87, 93, 83, 234, 48, 16, 125, 191, 191, 162,
|
||||
147, 119, 44, 32, 42, 50, 86, 71, 113, 28, 153, 97,
|
||||
240, 142, 232, 15, 88, 210, 45, 228, 154, 38, 189, 73,
|
||||
42, 226, 175, 191, 73, 105, 17, 108, 169, 223, 195, 229,
|
||||
1, 154, 179, 205, 246, 236, 57, 219, 217, 112, 114, 246,
|
||||
20, 115, 239, 17, 149, 102, 82, 4, 164, 181, 215, 36,
|
||||
30, 10, 42, 67, 38, 166, 1, 185, 191, 187, 106, 116,
|
||||
137, 167, 13, 136, 16, 184, 20, 24, 144, 5, 106, 114,
|
||||
118, 250, 235, 100, 222, 211, 102, 193, 81, 123, 118, 191,
|
||||
208, 189, 121, 64, 102, 198, 36, 61, 223, 215, 116, 134,
|
||||
49, 232, 61, 153, 160, 176, 177, 72, 170, 24, 140, 93,
|
||||
170, 169, 63, 151, 42, 76, 148, 164, 168, 181, 77, 31,
|
||||
115, 191, 221, 108, 30, 250, 49, 48, 65, 138, 52, 173,
|
||||
78, 41, 81, 204, 168, 146, 90, 70, 102, 143, 202, 216,
|
||||
151, 81, 196, 40, 102, 169, 236, 246, 86, 51, 187, 138,
|
||||
121, 145, 32, 166, 239, 33, 18, 131, 122, 72, 147, 134,
|
||||
205, 151, 128, 97, 19, 198, 153, 89, 100, 100, 136, 23,
|
||||
211, 222, 96, 42, 164, 130, 9, 183, 213, 90, 62, 228,
|
||||
212, 214, 26, 74, 122, 137, 17, 164, 220, 104, 183, 84,
|
||||
191, 85, 190, 204, 87, 217, 207, 149, 20, 70, 123, 243,
|
||||
30, 104, 202, 88, 64, 134, 108, 130, 202, 166, 151, 194,
|
||||
27, 163, 98, 17, 177, 161, 217, 185, 208, 91, 66, 8,
|
||||
218, 156, 107, 6, 1, 25, 203, 84, 81, 244, 174, 193,
|
||||
70, 65, 104, 175, 63, 242, 110, 113, 154, 114, 80, 238,
|
||||
62, 170, 237, 126, 57, 99, 198, 187, 196, 71, 16, 48,
|
||||
5, 197, 136, 239, 8, 232, 231, 190, 123, 252, 35, 240,
|
||||
128, 180, 59, 75, 140, 131, 152, 22, 24, 138, 198, 253,
|
||||
120, 243, 81, 207, 179, 70, 127, 228, 160, 9, 11, 45,
|
||||
175, 25, 107, 12, 70, 110, 163, 159, 87, 229, 191, 174,
|
||||
53, 121, 189, 202, 238, 89, 251, 93, 69, 253, 146, 106,
|
||||
89, 195, 216, 103, 153, 69, 98, 165, 77, 64, 193, 84,
|
||||
65, 50, 115, 143, 207, 66, 131, 48, 32, 35, 231, 18,
|
||||
207, 52, 23, 16, 99, 65, 61, 135, 179, 146, 254, 94,
|
||||
101, 78, 250, 43, 2, 243, 222, 156, 133, 114, 222, 183,
|
||||
242, 43, 201, 139, 45, 17, 112, 141, 203, 29, 174, 184,
|
||||
2, 110, 46, 161, 63, 180, 0, 56, 70, 38, 47, 57,
|
||||
207, 246, 191, 248, 73, 37, 151, 106, 197, 219, 125, 206,
|
||||
11, 163, 95, 219, 252, 51, 214, 103, 174, 148, 157, 163,
|
||||
51, 107, 29, 53, 168, 54, 156, 187, 72, 57, 71, 163,
|
||||
75, 214, 21, 120, 201, 187, 45, 50, 223, 216, 215, 117,
|
||||
188, 136, 39, 146, 175, 9, 188, 9, 190, 20, 177, 137,
|
||||
59, 41, 215, 144, 47, 20, 51, 100, 218, 12, 97, 130,
|
||||
188, 85, 170, 103, 21, 242, 90, 111, 215, 244, 3, 140,
|
||||
218, 53, 140, 218, 59, 97, 180, 95, 195, 104, 127, 39,
|
||||
140, 58, 53, 140, 58, 59, 97, 116, 80, 195, 232, 96,
|
||||
39, 140, 14, 107, 24, 29, 238, 132, 209, 81, 13, 163,
|
||||
163, 157, 48, 234, 214, 48, 234, 238, 132, 209, 113, 13,
|
||||
163, 227, 159, 97, 84, 61, 168, 175, 17, 220, 25, 177,
|
||||
68, 167, 192, 151, 131, 23, 52, 134, 55, 162, 106, 138,
|
||||
11, 124, 50, 5, 126, 103, 175, 47, 100, 184, 216, 58,
|
||||
223, 31, 16, 147, 145, 189, 105, 57, 235, 18, 160, 44,
|
||||
27, 109, 19, 180, 135, 58, 116, 19, 175, 233, 184, 65,
|
||||
100, 101, 179, 103, 216, 118, 243, 195, 243, 220, 142, 229,
|
||||
45, 227, 60, 143, 124, 199, 233, 108, 53, 160, 187, 21,
|
||||
67, 187, 251, 21, 51, 86, 2, 150, 134, 175, 5, 61,
|
||||
23, 125, 211, 143, 92, 171, 23, 113, 57, 19, 120, 155,
|
||||
186, 67, 48, 164, 70, 146, 28, 113, 76, 187, 100, 77,
|
||||
251, 13, 229, 59, 85, 202, 127, 182, 40, 215, 216, 149,
|
||||
221, 94, 89, 203, 102, 15, 109, 28, 76, 171, 95, 131,
|
||||
42, 151, 62, 75, 181, 15, 137, 235, 150, 18, 219, 2,
|
||||
127, 75, 252, 138, 150, 215, 105, 146, 40, 251, 71, 105,
|
||||
104, 69, 31, 165, 177, 109, 72, 189, 165, 251, 93, 191,
|
||||
127, 160, 251, 183, 119, 40, 91, 126, 247, 245, 187, 15,
|
||||
153, 159, 213, 107, 32, 66, 124, 42, 169, 181, 68, 191,
|
||||
77, 171, 239, 176, 191, 184, 210, 167, 255, 0, 80, 75,
|
||||
7, 8, 90, 249, 201, 152, 9, 3, 0, 0, 41, 15,
|
||||
0, 0, 80, 75, 3, 4, 20, 0, 8, 8, 8, 0,
|
||||
7, 140, 222, 74, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 18, 0, 0, 0, 119, 111, 114, 100,
|
||||
47, 110, 117, 109, 98, 101, 114, 105, 110, 103, 46, 120,
|
||||
109, 108, 213, 88, 75, 110, 219, 48, 16, 221, 247, 20,
|
||||
6, 247, 14, 101, 217, 113, 12, 35, 114, 80, 160, 48,
|
||||
208, 46, 218, 2, 77, 15, 64, 83, 148, 77, 128, 31,
|
||||
129, 164, 236, 100, 219, 67, 180, 55, 232, 162, 23, 232,
|
||||
125, 122, 129, 94, 161, 35, 74, 254, 40, 73, 85, 33,
|
||||
49, 80, 106, 69, 105, 222, 252, 248, 40, 204, 12, 117,
|
||||
125, 115, 39, 197, 96, 203, 140, 229, 90, 37, 104, 116,
|
||||
17, 161, 1, 83, 84, 167, 92, 173, 19, 244, 249, 118,
|
||||
57, 156, 161, 129, 117, 68, 165, 68, 104, 197, 18, 116,
|
||||
207, 44, 186, 89, 188, 186, 222, 205, 85, 33, 87, 204,
|
||||
128, 222, 0, 92, 40, 59, 223, 37, 104, 227, 92, 62,
|
||||
199, 216, 210, 13, 147, 196, 94, 232, 156, 41, 192, 50,
|
||||
109, 36, 113, 240, 106, 214, 120, 167, 77, 154, 27, 77,
|
||||
153, 181, 96, 41, 5, 142, 163, 104, 138, 37, 225, 10,
|
||||
213, 110, 116, 130, 10, 163, 230, 181, 143, 161, 228, 212,
|
||||
104, 171, 51, 55, 164, 90, 206, 117, 150, 113, 202, 234,
|
||||
101, 111, 97, 186, 4, 174, 76, 222, 104, 90, 72, 166,
|
||||
92, 21, 214, 48, 65, 28, 236, 219, 110, 120, 110, 247,
|
||||
222, 182, 109, 241, 183, 82, 160, 5, 236, 157, 172, 172,
|
||||
51, 132, 186, 247, 133, 28, 52, 222, 222, 166, 64, 162,
|
||||
87, 17, 91, 1, 16, 135, 37, 65, 145, 151, 0, 141,
|
||||
198, 129, 108, 75, 68, 169, 132, 23, 21, 137, 75, 121,
|
||||
16, 174, 10, 33, 152, 171, 16, 48, 188, 101, 119, 7,
|
||||
232, 247, 151, 159, 7, 249, 59, 186, 151, 10, 150, 213,
|
||||
234, 249, 71, 83, 46, 14, 114, 169, 215, 189, 14, 132,
|
||||
64, 240, 156, 107, 155, 160, 171, 56, 42, 213, 241, 81,
|
||||
145, 171, 20, 192, 210, 79, 133, 194, 203, 134, 168, 181,
|
||||
63, 254, 241, 116, 175, 93, 123, 55, 245, 178, 212, 202,
|
||||
217, 114, 227, 150, 114, 158, 160, 79, 247, 114, 165, 133,
|
||||
55, 125, 173, 108, 67, 64, 109, 3, 230, 10, 194, 164,
|
||||
44, 35, 133, 168, 243, 62, 248, 42, 53, 63, 192, 185,
|
||||
213, 218, 85, 92, 31, 16, 251, 77, 63, 228, 116, 244,
|
||||
114, 78, 127, 125, 251, 126, 6, 78, 71, 209, 172, 141,
|
||||
84, 15, 63, 135, 213, 19, 50, 142, 204, 54, 133, 15,
|
||||
56, 59, 51, 195, 241, 25, 24, 254, 250, 227, 28, 12,
|
||||
79, 38, 173, 12, 151, 112, 47, 25, 30, 135, 82, 23,
|
||||
70, 179, 168, 149, 225, 18, 238, 77, 101, 152, 132, 82,
|
||||
25, 226, 209, 180, 141, 85, 15, 247, 242, 187, 189, 12,
|
||||
165, 50, 196, 151, 173, 13, 205, 195, 189, 100, 120, 26,
|
||||
74, 101, 136, 103, 173, 221, 205, 195, 189, 169, 12, 87,
|
||||
161, 84, 134, 113, 220, 218, 209, 60, 220, 203, 239, 118,
|
||||
22, 74, 101, 0, 198, 90, 25, 158, 62, 179, 163, 253,
|
||||
7, 134, 113, 227, 134, 241, 207, 235, 71, 252, 236, 235,
|
||||
135, 130, 27, 94, 37, 183, 69, 150, 29, 165, 14, 242,
|
||||
95, 63, 121, 86, 93, 14, 234, 148, 246, 38, 231, 13,
|
||||
198, 95, 54, 229, 7, 152, 122, 215, 241, 57, 192, 212,
|
||||
187, 206, 165, 1, 166, 222, 117, 248, 11, 48, 245, 174,
|
||||
83, 85, 128, 169, 119, 29, 87, 2, 76, 189, 235, 76,
|
||||
16, 96, 234, 93, 155, 109, 16, 169, 63, 238, 98, 202,
|
||||
119, 47, 117, 250, 211, 172, 209, 202, 26, 123, 194, 94,
|
||||
243, 145, 89, 252, 119, 179, 248, 212, 12, 159, 252, 175,
|
||||
92, 252, 1, 80, 75, 7, 8, 15, 16, 133, 144, 101,
|
||||
2, 0, 0, 245, 20, 0, 0, 80, 75, 3, 4, 20,
|
||||
0, 8, 8, 8, 0, 7, 140, 222, 74, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0,
|
||||
0, 119, 111, 114, 100, 47, 115, 101, 116, 116, 105, 110,
|
||||
103, 115, 46, 120, 109, 108, 69, 79, 203, 78, 195, 64,
|
||||
12, 188, 243, 21, 145, 239, 116, 23, 14, 60, 162, 36,
|
||||
21, 151, 158, 184, 81, 62, 192, 77, 220, 116, 165, 172,
|
||||
189, 90, 155, 6, 248, 122, 12, 85, 197, 109, 70, 243,
|
||||
208, 76, 183, 253, 204, 75, 115, 166, 170, 73, 184, 135,
|
||||
187, 77, 132, 134, 120, 148, 41, 241, 220, 195, 251, 126,
|
||||
119, 251, 4, 141, 26, 242, 132, 139, 48, 245, 240, 69,
|
||||
10, 219, 225, 166, 91, 91, 37, 51, 119, 105, 227, 13,
|
||||
172, 237, 218, 195, 201, 172, 180, 33, 232, 120, 162, 140,
|
||||
186, 145, 66, 236, 218, 81, 106, 70, 115, 90, 231, 176,
|
||||
74, 157, 74, 149, 145, 84, 61, 154, 151, 112, 31, 227,
|
||||
67, 200, 152, 24, 6, 175, 252, 22, 201, 205, 218, 22,
|
||||
170, 35, 177, 249, 156, 24, 33, 252, 10, 19, 29, 241,
|
||||
99, 177, 61, 30, 222, 76, 138, 91, 206, 184, 244, 240,
|
||||
24, 159, 47, 242, 40, 185, 160, 13, 93, 248, 135, 107,
|
||||
107, 190, 130, 118, 194, 246, 138, 60, 95, 35, 224, 128,
|
||||
80, 237, 69, 19, 94, 216, 33, 77, 201, 81, 248, 75,
|
||||
95, 63, 13, 63, 80, 75, 7, 8, 201, 163, 110, 75,
|
||||
202, 0, 0, 0, 24, 1, 0, 0, 80, 75, 3, 4,
|
||||
20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0,
|
||||
0, 0, 119, 111, 114, 100, 47, 100, 111, 99, 117, 109,
|
||||
101, 110, 116, 46, 120, 109, 108, 237, 87, 219, 78, 227,
|
||||
48, 16, 125, 223, 175, 48, 121, 167, 105, 216, 46, 130,
|
||||
136, 22, 129, 184, 85, 98, 161, 82, 131, 208, 62, 186,
|
||||
142, 147, 88, 235, 155, 198, 78, 75, 249, 122, 236, 230,
|
||||
198, 101, 133, 186, 116, 197, 238, 34, 94, 234, 198, 51,
|
||||
115, 230, 204, 153, 177, 18, 31, 28, 222, 9, 142, 230,
|
||||
20, 12, 83, 114, 24, 68, 189, 126, 128, 168, 36, 42,
|
||||
101, 50, 31, 6, 55, 201, 217, 246, 94, 128, 140, 197,
|
||||
50, 197, 92, 73, 58, 12, 150, 212, 4, 135, 163, 47,
|
||||
7, 139, 56, 85, 164, 20, 84, 90, 228, 16, 164, 137,
|
||||
213, 48, 40, 65, 198, 134, 20, 84, 96, 179, 45, 24,
|
||||
1, 101, 84, 102, 183, 137, 18, 177, 202, 50, 70, 104,
|
||||
189, 4, 117, 4, 12, 131, 194, 90, 29, 135, 97, 29,
|
||||
212, 83, 154, 74, 103, 203, 20, 8, 108, 221, 35, 228,
|
||||
97, 21, 114, 82, 231, 10, 119, 250, 253, 221, 16, 40,
|
||||
199, 214, 241, 53, 5, 211, 166, 65, 155, 191, 150, 127,
|
||||
46, 120, 227, 183, 88, 39, 235, 66, 65, 170, 65, 17,
|
||||
106, 140, 19, 66, 240, 42, 175, 192, 76, 182, 48, 81,
|
||||
127, 141, 130, 61, 78, 27, 161, 215, 201, 156, 2, 94,
|
||||
60, 74, 249, 148, 200, 73, 101, 236, 16, 205, 11, 200,
|
||||
150, 70, 207, 209, 168, 213, 91, 161, 56, 188, 168, 255,
|
||||
12, 111, 90, 96, 77, 59, 180, 124, 51, 180, 115, 80,
|
||||
165, 110, 208, 4, 89, 167, 90, 129, 225, 103, 169, 189,
|
||||
98, 218, 117, 116, 198, 56, 179, 203, 85, 225, 29, 169,
|
||||
104, 176, 25, 171, 231, 154, 189, 13, 207, 207, 143, 32,
|
||||
241, 56, 151, 10, 240, 140, 187, 131, 224, 128, 144, 103,
|
||||
23, 140, 220, 89, 152, 169, 116, 233, 87, 189, 250, 153,
|
||||
192, 106, 153, 218, 37, 167, 104, 17, 207, 49, 31, 6,
|
||||
87, 190, 106, 30, 132, 222, 2, 222, 33, 236, 214, 58,
|
||||
0, 90, 155, 91, 206, 148, 180, 198, 5, 19, 215, 226,
|
||||
11, 202, 231, 212, 50, 130, 3, 183, 129, 13, 97, 236,
|
||||
217, 94, 113, 36, 205, 147, 189, 85, 26, 115, 223, 36,
|
||||
223, 25, 248, 157, 176, 69, 183, 35, 231, 202, 21, 186,
|
||||
85, 192, 211, 158, 55, 216, 202, 92, 209, 249, 205, 74,
|
||||
30, 177, 93, 139, 220, 139, 170, 94, 101, 251, 254, 250,
|
||||
188, 89, 136, 191, 219, 210, 164, 96, 230, 73, 43, 223,
|
||||
53, 189, 63, 94, 177, 209, 152, 184, 163, 161, 129, 26,
|
||||
10, 115, 26, 140, 16, 51, 200, 22, 20, 1, 197, 28,
|
||||
113, 150, 209, 207, 97, 251, 24, 195, 54, 182, 190, 181,
|
||||
82, 89, 148, 97, 105, 177, 89, 126, 54, 246, 99, 52,
|
||||
246, 72, 160, 49, 34, 128, 239, 151, 135, 159, 29, 221,
|
||||
188, 163, 178, 20, 149, 11, 227, 115, 222, 56, 244, 91,
|
||||
219, 56, 109, 246, 162, 58, 83, 27, 240, 15, 204, 194,
|
||||
241, 205, 229, 229, 105, 130, 174, 175, 78, 55, 24, 133,
|
||||
143, 160, 64, 114, 123, 253, 167, 21, 136, 254, 19, 5,
|
||||
46, 174, 47, 127, 160, 233, 197, 56, 217, 66, 211, 155,
|
||||
99, 84, 9, 178, 245, 75, 53, 12, 37, 182, 14, 91,
|
||||
234, 86, 9, 73, 239, 236, 4, 231, 180, 74, 165, 243,
|
||||
169, 79, 230, 46, 97, 81, 180, 239, 63, 243, 29, 39,
|
||||
247, 127, 119, 239, 235, 94, 227, 240, 29, 131, 219, 229,
|
||||
52, 179, 206, 48, 24, 244, 189, 15, 176, 188, 120, 244,
|
||||
88, 80, 156, 82, 240, 99, 228, 30, 172, 210, 157, 37,
|
||||
83, 202, 182, 150, 153, 178, 86, 137, 206, 152, 151, 182,
|
||||
54, 214, 169, 174, 74, 145, 84, 84, 51, 225, 224, 83,
|
||||
74, 88, 219, 53, 127, 77, 153, 128, 123, 193, 213, 117,
|
||||
100, 152, 155, 186, 8, 235, 74, 58, 97, 224, 202, 117,
|
||||
151, 208, 198, 206, 33, 153, 85, 102, 119, 49, 62, 7,
|
||||
230, 187, 234, 117, 240, 176, 25, 46, 185, 245, 12, 56,
|
||||
147, 116, 194, 44, 41, 188, 212, 43, 78, 164, 192, 48,
|
||||
173, 62, 155, 6, 59, 251, 131, 253, 221, 40, 250, 214,
|
||||
204, 65, 35, 104, 216, 92, 48, 194, 238, 214, 61, 122,
|
||||
0, 80, 75, 7, 8, 175, 55, 157, 93, 214, 2, 0,
|
||||
0, 186, 15, 0, 0, 80, 75, 3, 4, 20, 0, 8,
|
||||
8, 8, 0, 7, 140, 222, 74, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 119,
|
||||
111, 114, 100, 47, 116, 104, 101, 109, 101, 47, 116, 104,
|
||||
101, 109, 101, 49, 46, 120, 109, 108, 229, 88, 75, 111,
|
||||
227, 54, 16, 190, 247, 87, 16, 186, 239, 210, 122, 57,
|
||||
114, 16, 103, 177, 118, 44, 244, 208, 22, 69, 226, 162,
|
||||
103, 90, 162, 37, 109, 168, 7, 72, 38, 78, 254, 125,
|
||||
135, 212, 139, 178, 172, 196, 187, 241, 162, 5, 234, 131,
|
||||
77, 82, 223, 204, 55, 15, 114, 56, 242, 205, 151, 151,
|
||||
156, 161, 103, 202, 69, 86, 22, 75, 203, 254, 60, 179,
|
||||
16, 45, 162, 50, 206, 138, 100, 105, 253, 181, 13, 63,
|
||||
5, 214, 151, 219, 95, 110, 200, 181, 76, 105, 78, 17,
|
||||
160, 11, 113, 77, 150, 86, 42, 101, 117, 141, 177, 136,
|
||||
96, 153, 136, 207, 101, 69, 11, 120, 182, 47, 121, 78,
|
||||
36, 76, 121, 130, 99, 78, 14, 160, 37, 103, 216, 153,
|
||||
205, 230, 56, 39, 89, 97, 161, 130, 228, 116, 105, 221,
|
||||
209, 61, 121, 98, 18, 109, 149, 78, 235, 182, 213, 190,
|
||||
97, 240, 85, 72, 161, 22, 34, 198, 31, 34, 77, 57,
|
||||
16, 209, 224, 248, 209, 86, 63, 226, 85, 172, 25, 71,
|
||||
207, 132, 45, 45, 96, 138, 203, 195, 150, 190, 72, 11,
|
||||
49, 34, 36, 60, 88, 90, 51, 253, 177, 240, 237, 13,
|
||||
238, 132, 152, 156, 144, 53, 228, 66, 253, 105, 228, 26,
|
||||
129, 248, 209, 209, 114, 60, 217, 117, 130, 118, 232, 45,
|
||||
174, 238, 58, 253, 78, 173, 127, 140, 219, 108, 54, 235,
|
||||
141, 221, 233, 211, 0, 18, 69, 224, 170, 61, 194, 122,
|
||||
97, 96, 175, 90, 157, 6, 168, 30, 142, 117, 175, 103,
|
||||
254, 204, 27, 226, 13, 253, 238, 8, 191, 88, 173, 86,
|
||||
254, 98, 128, 119, 123, 188, 55, 194, 7, 179, 185, 247,
|
||||
213, 25, 224, 189, 30, 239, 143, 237, 95, 125, 93, 175,
|
||||
231, 3, 188, 223, 227, 231, 35, 124, 120, 181, 152, 123,
|
||||
67, 188, 6, 165, 44, 43, 30, 71, 104, 149, 207, 46,
|
||||
51, 29, 100, 95, 178, 95, 79, 194, 3, 128, 7, 237,
|
||||
6, 232, 81, 216, 216, 94, 181, 124, 33, 39, 55, 91,
|
||||
78, 190, 149, 60, 4, 132, 206, 46, 145, 89, 129, 228,
|
||||
107, 5, 128, 8, 128, 219, 44, 167, 2, 253, 65, 15,
|
||||
232, 190, 204, 73, 161, 152, 200, 53, 37, 6, 162, 94,
|
||||
138, 196, 209, 18, 62, 82, 156, 103, 197, 79, 98, 233,
|
||||
21, 99, 211, 83, 237, 119, 62, 237, 246, 62, 99, 236,
|
||||
65, 190, 50, 250, 155, 208, 54, 137, 146, 101, 113, 8,
|
||||
139, 122, 162, 165, 186, 48, 87, 41, 12, 27, 190, 1,
|
||||
46, 225, 68, 143, 17, 47, 229, 223, 153, 76, 31, 82,
|
||||
82, 1, 143, 173, 25, 18, 209, 168, 78, 4, 170, 74,
|
||||
1, 201, 181, 38, 117, 235, 18, 145, 21, 178, 94, 243,
|
||||
219, 99, 13, 104, 34, 127, 47, 227, 122, 217, 53, 143,
|
||||
123, 167, 70, 207, 18, 97, 18, 185, 74, 193, 185, 100,
|
||||
238, 213, 199, 200, 236, 26, 120, 38, 155, 237, 159, 102,
|
||||
243, 223, 100, 195, 70, 52, 97, 139, 35, 162, 138, 183,
|
||||
61, 119, 106, 106, 36, 34, 194, 104, 172, 226, 94, 43,
|
||||
104, 211, 114, 241, 20, 137, 148, 196, 180, 201, 145, 125,
|
||||
210, 17, 219, 61, 51, 108, 193, 251, 81, 51, 216, 22,
|
||||
238, 199, 216, 206, 73, 146, 73, 231, 77, 208, 249, 23,
|
||||
200, 210, 108, 148, 37, 60, 62, 142, 172, 24, 206, 208,
|
||||
1, 172, 242, 29, 223, 66, 17, 169, 150, 214, 30, 74,
|
||||
8, 12, 243, 10, 244, 137, 34, 177, 16, 97, 9, 92,
|
||||
239, 145, 108, 92, 121, 247, 48, 31, 59, 124, 122, 91,
|
||||
218, 179, 73, 135, 7, 20, 21, 23, 242, 142, 136, 180,
|
||||
150, 210, 143, 218, 219, 176, 232, 237, 119, 124, 79, 197,
|
||||
225, 50, 14, 156, 168, 70, 231, 89, 225, 6, 246, 191,
|
||||
104, 5, 62, 78, 45, 221, 239, 105, 36, 39, 86, 250,
|
||||
105, 243, 172, 124, 146, 148, 63, 164, 241, 1, 237, 216,
|
||||
19, 191, 39, 96, 183, 87, 239, 174, 56, 19, 18, 66,
|
||||
220, 78, 160, 203, 241, 189, 102, 227, 13, 79, 126, 115,
|
||||
10, 142, 111, 221, 230, 116, 16, 86, 165, 164, 169, 73,
|
||||
129, 145, 251, 26, 174, 199, 157, 13, 122, 102, 152, 135,
|
||||
39, 108, 255, 65, 87, 220, 11, 186, 226, 255, 127, 93,
|
||||
81, 59, 151, 22, 212, 141, 117, 7, 1, 125, 0, 39,
|
||||
72, 237, 209, 165, 85, 114, 153, 150, 80, 133, 170, 52,
|
||||
139, 66, 14, 157, 131, 230, 2, 187, 160, 83, 150, 202,
|
||||
36, 196, 212, 59, 131, 178, 149, 62, 247, 117, 171, 214,
|
||||
81, 23, 185, 36, 149, 247, 89, 130, 120, 6, 149, 78,
|
||||
166, 156, 210, 63, 101, 227, 231, 59, 202, 108, 199, 188,
|
||||
95, 91, 69, 77, 157, 233, 204, 21, 85, 253, 187, 163,
|
||||
207, 148, 109, 213, 233, 157, 43, 255, 45, 148, 182, 213,
|
||||
164, 9, 132, 198, 29, 39, 13, 159, 58, 93, 187, 36,
|
||||
252, 15, 119, 62, 222, 68, 231, 243, 118, 123, 208, 19,
|
||||
121, 223, 211, 139, 120, 70, 209, 55, 174, 130, 197, 199,
|
||||
76, 248, 206, 171, 214, 57, 237, 177, 227, 159, 125, 213,
|
||||
86, 68, 166, 72, 125, 65, 225, 206, 120, 196, 104, 215,
|
||||
223, 110, 203, 123, 200, 62, 234, 58, 74, 4, 27, 241,
|
||||
83, 208, 28, 191, 110, 113, 7, 54, 7, 134, 115, 74,
|
||||
213, 207, 109, 163, 250, 20, 4, 19, 249, 190, 100, 243,
|
||||
105, 4, 219, 157, 8, 246, 219, 116, 63, 30, 108, 255,
|
||||
68, 172, 253, 183, 67, 141, 199, 71, 20, 27, 111, 50,
|
||||
122, 54, 250, 51, 161, 220, 125, 3, 238, 230, 245, 70,
|
||||
212, 175, 79, 47, 146, 147, 117, 251, 22, 8, 122, 112,
|
||||
47, 122, 251, 15, 80, 75, 7, 8, 58, 137, 128, 168,
|
||||
212, 3, 0, 0, 22, 17, 0, 0, 80, 75, 3, 4,
|
||||
20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0,
|
||||
0, 0, 119, 111, 114, 100, 47, 102, 111, 110, 116, 84,
|
||||
97, 98, 108, 101, 46, 120, 109, 108, 197, 147, 221, 106,
|
||||
194, 64, 16, 133, 239, 251, 20, 203, 222, 215, 141, 94,
|
||||
148, 18, 140, 82, 40, 165, 23, 173, 133, 170, 15, 48,
|
||||
110, 38, 102, 97, 127, 194, 206, 38, 169, 111, 223, 53,
|
||||
42, 136, 141, 208, 22, 209, 187, 236, 206, 236, 57, 223,
|
||||
28, 38, 227, 233, 151, 209, 172, 65, 79, 202, 217, 140,
|
||||
15, 7, 9, 103, 104, 165, 203, 149, 93, 103, 124, 185,
|
||||
120, 185, 127, 228, 140, 2, 216, 28, 180, 179, 152, 241,
|
||||
13, 18, 159, 78, 238, 198, 109, 90, 56, 27, 136, 197,
|
||||
231, 150, 210, 54, 227, 101, 8, 85, 42, 4, 201, 18,
|
||||
13, 208, 192, 85, 104, 99, 173, 112, 222, 64, 136, 71,
|
||||
191, 22, 173, 243, 121, 229, 157, 68, 162, 168, 110, 180,
|
||||
24, 37, 201, 131, 48, 160, 44, 223, 203, 248, 223, 200,
|
||||
184, 162, 80, 18, 159, 157, 172, 13, 218, 176, 19, 241,
|
||||
168, 33, 196, 9, 168, 84, 21, 241, 201, 158, 142, 181,
|
||||
169, 5, 19, 161, 23, 202, 32, 177, 25, 182, 236, 211,
|
||||
25, 176, 93, 131, 44, 193, 19, 110, 123, 26, 208, 25,
|
||||
79, 18, 46, 186, 119, 96, 148, 222, 28, 110, 125, 215,
|
||||
222, 21, 42, 21, 100, 121, 184, 111, 192, 43, 88, 105,
|
||||
220, 150, 196, 206, 236, 135, 233, 124, 99, 86, 78, 247,
|
||||
122, 141, 46, 237, 245, 20, 91, 250, 173, 122, 199, 162,
|
||||
86, 17, 253, 211, 234, 77, 173, 208, 119, 97, 179, 57,
|
||||
122, 85, 116, 174, 160, 195, 44, 86, 15, 58, 167, 121,
|
||||
139, 62, 178, 225, 165, 67, 248, 136, 187, 114, 20, 250,
|
||||
9, 83, 23, 17, 91, 90, 21, 151, 27, 217, 251, 252,
|
||||
74, 80, 199, 113, 129, 165, 179, 100, 87, 194, 121, 69,
|
||||
221, 96, 80, 18, 110, 13, 242, 215, 191, 3, 234, 224,
|
||||
122, 172, 114, 44, 160, 214, 225, 38, 107, 113, 6, 169,
|
||||
119, 250, 253, 7, 77, 190, 1, 80, 75, 7, 8, 27,
|
||||
51, 25, 108, 79, 1, 0, 0, 113, 5, 0, 0, 80,
|
||||
75, 3, 4, 20, 0, 8, 8, 8, 0, 7, 140, 222,
|
||||
74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 19, 0, 0, 0, 91, 67, 111, 110, 116, 101, 110,
|
||||
116, 95, 84, 121, 112, 101, 115, 93, 46, 120, 109, 108,
|
||||
189, 148, 205, 78, 195, 48, 16, 132, 239, 125, 138, 200,
|
||||
87, 148, 184, 229, 128, 16, 74, 218, 3, 18, 71, 232,
|
||||
161, 156, 145, 107, 111, 90, 139, 248, 71, 94, 183, 180,
|
||||
111, 207, 58, 13, 17, 66, 72, 105, 105, 203, 197, 82,
|
||||
228, 153, 249, 198, 155, 196, 229, 108, 103, 154, 108, 11,
|
||||
1, 181, 179, 21, 155, 20, 99, 150, 129, 149, 78, 105,
|
||||
187, 170, 216, 235, 226, 41, 191, 103, 179, 233, 168, 92,
|
||||
236, 61, 96, 70, 90, 139, 21, 91, 199, 232, 31, 56,
|
||||
71, 185, 6, 35, 176, 112, 30, 44, 237, 212, 46, 24,
|
||||
17, 233, 49, 172, 184, 23, 242, 93, 172, 128, 223, 142,
|
||||
199, 119, 92, 58, 27, 193, 198, 60, 166, 12, 54, 45,
|
||||
95, 8, 23, 180, 130, 108, 46, 66, 124, 22, 6, 42,
|
||||
198, 223, 2, 52, 200, 139, 180, 178, 236, 241, 96, 72,
|
||||
204, 138, 9, 239, 27, 45, 69, 164, 126, 124, 107, 213,
|
||||
15, 90, 222, 145, 146, 179, 213, 224, 90, 123, 188, 33,
|
||||
1, 227, 191, 147, 148, 147, 243, 224, 60, 114, 10, 46,
|
||||
146, 238, 36, 156, 171, 107, 45, 129, 50, 54, 134, 44,
|
||||
5, 236, 200, 169, 64, 229, 158, 34, 33, 68, 13, 199,
|
||||
177, 165, 11, 112, 58, 252, 235, 172, 201, 125, 36, 241,
|
||||
195, 5, 213, 13, 183, 47, 77, 242, 255, 24, 116, 139,
|
||||
198, 184, 111, 0, 207, 158, 115, 202, 162, 3, 75, 64,
|
||||
164, 15, 147, 250, 31, 114, 7, 241, 118, 99, 150, 16,
|
||||
200, 114, 249, 6, 125, 244, 240, 12, 32, 70, 210, 93,
|
||||
99, 10, 93, 242, 96, 133, 239, 239, 254, 178, 21, 250,
|
||||
29, 35, 180, 29, 236, 17, 233, 198, 128, 195, 58, 57,
|
||||
187, 75, 27, 51, 136, 172, 9, 176, 16, 203, 230, 15,
|
||||
255, 219, 208, 217, 251, 232, 174, 196, 168, 228, 237, 61,
|
||||
57, 253, 4, 80, 75, 7, 8, 113, 70, 144, 68, 71,
|
||||
1, 0, 0, 86, 5, 0, 0, 80, 75, 1, 2, 20,
|
||||
0, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 232,
|
||||
208, 1, 35, 217, 0, 0, 0, 61, 2, 0, 0, 11,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 95, 114, 101, 108, 115, 47, 46,
|
||||
114, 101, 108, 115, 80, 75, 1, 2, 20, 0, 20, 0,
|
||||
8, 8, 8, 0, 7, 140, 222, 74, 126, 47, 253, 199,
|
||||
37, 1, 0, 0, 0, 2, 0, 0, 16, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 1,
|
||||
0, 0, 100, 111, 99, 80, 114, 111, 112, 115, 47, 97,
|
||||
112, 112, 46, 120, 109, 108, 80, 75, 1, 2, 20, 0,
|
||||
20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 45, 164,
|
||||
180, 116, 82, 1, 0, 0, 138, 2, 0, 0, 17, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
117, 2, 0, 0, 100, 111, 99, 80, 114, 111, 112, 115,
|
||||
47, 99, 111, 114, 101, 46, 120, 109, 108, 80, 75, 1,
|
||||
2, 20, 0, 20, 0, 8, 8, 8, 0, 7, 140, 222,
|
||||
74, 41, 11, 4, 96, 232, 0, 0, 0, 28, 3, 0,
|
||||
0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 6, 4, 0, 0, 119, 111, 114, 100, 47,
|
||||
95, 114, 101, 108, 115, 47, 100, 111, 99, 117, 109, 101,
|
||||
110, 116, 46, 120, 109, 108, 46, 114, 101, 108, 115, 80,
|
||||
75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 7,
|
||||
140, 222, 74, 90, 249, 201, 152, 9, 3, 0, 0, 41,
|
||||
15, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 56, 5, 0, 0, 119, 111, 114,
|
||||
100, 47, 115, 116, 121, 108, 101, 115, 46, 120, 109, 108,
|
||||
80, 75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0,
|
||||
7, 140, 222, 74, 15, 16, 133, 144, 101, 2, 0, 0,
|
||||
245, 20, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 126, 8, 0, 0, 119, 111,
|
||||
114, 100, 47, 110, 117, 109, 98, 101, 114, 105, 110, 103,
|
||||
46, 120, 109, 108, 80, 75, 1, 2, 20, 0, 20, 0,
|
||||
8, 8, 8, 0, 7, 140, 222, 74, 201, 163, 110, 75,
|
||||
202, 0, 0, 0, 24, 1, 0, 0, 17, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 11,
|
||||
0, 0, 119, 111, 114, 100, 47, 115, 101, 116, 116, 105,
|
||||
110, 103, 115, 46, 120, 109, 108, 80, 75, 1, 2, 20,
|
||||
0, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 175,
|
||||
55, 157, 93, 214, 2, 0, 0, 186, 15, 0, 0, 17,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 44, 12, 0, 0, 119, 111, 114, 100, 47, 100, 111,
|
||||
99, 117, 109, 101, 110, 116, 46, 120, 109, 108, 80, 75,
|
||||
1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 7, 140,
|
||||
222, 74, 58, 137, 128, 168, 212, 3, 0, 0, 22, 17,
|
||||
0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 65, 15, 0, 0, 119, 111, 114, 100,
|
||||
47, 116, 104, 101, 109, 101, 47, 116, 104, 101, 109, 101,
|
||||
49, 46, 120, 109, 108, 80, 75, 1, 2, 20, 0, 20,
|
||||
0, 8, 8, 8, 0, 7, 140, 222, 74, 27, 51, 25,
|
||||
108, 79, 1, 0, 0, 113, 5, 0, 0, 18, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88,
|
||||
19, 0, 0, 119, 111, 114, 100, 47, 102, 111, 110, 116,
|
||||
84, 97, 98, 108, 101, 46, 120, 109, 108, 80, 75, 1,
|
||||
2, 20, 0, 20, 0, 8, 8, 8, 0, 7, 140, 222,
|
||||
74, 113, 70, 144, 68, 71, 1, 0, 0, 86, 5, 0,
|
||||
0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 231, 20, 0, 0, 91, 67, 111, 110, 116,
|
||||
101, 110, 116, 95, 84, 121, 112, 101, 115, 93, 46, 120,
|
||||
109, 108, 80, 75, 5, 6, 0, 0, 0, 0, 11, 0,
|
||||
11, 0, 191, 2, 0, 0, 111, 22, 0, 0, 0, 0}
|
||||
|
||||
func TestNewDocx(t *testing.T) {
|
||||
_, err := NewDocx(bytes.NewReader(testDocx), int64(len(testDocx)))
|
||||
if err != nil {
|
||||
t.Fatal("Failed to open valid docx data", err)
|
||||
}
|
||||
|
||||
_, err = NewDocx(bytes.NewReader([]byte{255}), 1)
|
||||
if err == nil {
|
||||
t.Error("Failed to get error with bad zip data")
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
zipWriter := zip.NewWriter(buf)
|
||||
fileWriter, err := zipWriter.Create("randomData")
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create zip writer", err)
|
||||
}
|
||||
|
||||
io.CopyN(fileWriter, rand.Reader, 128)
|
||||
zipWriter.Flush()
|
||||
zipWriter.Close()
|
||||
|
||||
_, err = NewDocx(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||
if err != ErrMissingDocument {
|
||||
t.Error("Failed to get err", ErrMissingDocument, "got", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestText(t *testing.T) {
|
||||
doc, err := NewDocx(bytes.NewReader(testDocx), int64(len(testDocx)))
|
||||
if err != nil {
|
||||
t.Fatal("Failed to open valid docx data", err)
|
||||
}
|
||||
|
||||
lines, err := doc.Text()
|
||||
if err != nil {
|
||||
t.Error("Failed to get text lines from valid doc", err)
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"Hello World.",
|
||||
"This",
|
||||
"is the real life.",
|
||||
"It is not fantasy.",
|
||||
"Am I crazy?",
|
||||
"BULLET ONE",
|
||||
"BULLET TWO",
|
||||
"HOLY SHIT! SUB BULLET!",
|
||||
}
|
||||
|
||||
if len(lines) != len(expected) {
|
||||
t.Errorf("len(lines) != len(expected): %d != %d", len(lines), len(expected))
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
if line != expected[i] {
|
||||
t.Errorf("line != expected[%d]: \"%s\" != \"%s\"", i, line, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
534
handler.go
Normal file
534
handler.go
Normal file
|
@ -0,0 +1,534 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/blevesearch/bleve"
|
||||
blevemapping "github.com/blevesearch/bleve/mapping"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tblyler/recipe-card/recipe"
|
||||
)
|
||||
|
||||
const (
|
||||
imagePattern = "/images/"
|
||||
stockImagePatten = "/stock-images/"
|
||||
recipePattern = "/recipe/"
|
||||
docxPattern = "/docx/"
|
||||
)
|
||||
|
||||
// Handler contains functions for http handlerfunc
|
||||
type Handler struct {
|
||||
recipePath string
|
||||
recipes map[string]*recipe.Recipe
|
||||
recipeSlice []*recipe.Recipe
|
||||
idx bleve.Index
|
||||
templates *template.Template
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func GetItemIndex(path string) (map[string][]byte, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
itemIndex := make(map[string][]byte)
|
||||
for {
|
||||
data, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return itemIndex, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// remove newline
|
||||
key := string(data[:len(data)-1])
|
||||
sha256sum := make([]byte, sha256.Size)
|
||||
|
||||
read, err := reader.Read(sha256sum)
|
||||
if err != nil && read != sha256.Size {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
itemIndex[key] = sha256sum
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return itemIndex, nil
|
||||
}
|
||||
|
||||
func SaveItemIndex(itemIndex map[string][]byte, path string) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
for key, sha256sum := range itemIndex {
|
||||
_, err = file.WriteString(key + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = file.Write(sha256sum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewHandler creates a new instance to handle HTTP requests
|
||||
func NewHandler(recipePath string, indexPath string, logger *log.Logger) (*Handler, error) {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
logger.Out = ioutil.Discard
|
||||
}
|
||||
|
||||
logger.WithField("recipePath", recipePath).Debugln("Getting absolute recipe path")
|
||||
|
||||
var err error
|
||||
recipePath, err = filepath.Abs(recipePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed absolute recipe path: %s", err.Error())
|
||||
}
|
||||
|
||||
logger.WithField("recipePath", recipePath).Debugln("Got absolute recipe path")
|
||||
|
||||
if indexPath != "" {
|
||||
logger.WithField("indexPath", indexPath).Debugln("Getting absolute index path")
|
||||
indexPath, err = filepath.Abs(indexPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed absolute index path: %s", err.Error())
|
||||
}
|
||||
|
||||
logger.WithField("indexPath", indexPath).Debugln("Got absolute index path")
|
||||
os.MkdirAll(indexPath, 0755)
|
||||
}
|
||||
|
||||
logger.WithField("recipePath", recipePath).Infoln("Getting recipes from path")
|
||||
recipeSlice, err := recipe.RecipesFromPath(recipePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Infof("Found %d recipes", len(recipeSlice))
|
||||
|
||||
handler := new(Handler)
|
||||
handler.logger = logger
|
||||
handler.recipePath = recipePath
|
||||
handler.recipeSlice = recipeSlice
|
||||
handler.recipes = make(map[string]*recipe.Recipe)
|
||||
bleveIndexPath := ""
|
||||
itemIndexPath := ""
|
||||
// this improves indexing performance a shit ton
|
||||
// I don't think it stores the document data, just analysis data
|
||||
// could be wrong, documentation is sparse for it
|
||||
// functionality seems the same for here though
|
||||
blevemapping.StoreDynamic = false
|
||||
|
||||
if indexPath == "" {
|
||||
logger.Info("Creating memory mapped search index")
|
||||
handler.idx, err = bleve.NewMemOnly(bleve.NewIndexMapping())
|
||||
} else {
|
||||
itemIndexPath = filepath.Join(indexPath, "item.idx")
|
||||
bleveIndexPath = filepath.Join(indexPath, "bleve")
|
||||
logger.WithField("bleveIndexPath", bleveIndexPath).Infoln("Trying to open index path")
|
||||
|
||||
handler.idx, err = bleve.Open(bleveIndexPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("bleveIndexPath", bleveIndexPath).Warnln(
|
||||
"Failed to open index path, trying to recreate it",
|
||||
)
|
||||
|
||||
handler.idx, err = bleve.New(bleveIndexPath, bleve.NewIndexMapping())
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Bleve open: %s", err.Error())
|
||||
}
|
||||
|
||||
var itemIndex map[string][]byte
|
||||
|
||||
if itemIndexPath != "" {
|
||||
logger.WithField("itemIndexPath", itemIndexPath).Debugln("Trying to open previous item index")
|
||||
itemIndex, err = GetItemIndex(itemIndexPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("itemIndexPath", itemIndexPath).Warnln("Failed to open previous item index")
|
||||
} else {
|
||||
logger.WithField("count", len(itemIndex)).Debugln("Got previous item indexes")
|
||||
}
|
||||
}
|
||||
|
||||
if itemIndex == nil {
|
||||
itemIndex = make(map[string][]byte)
|
||||
}
|
||||
|
||||
for _, recip := range recipeSlice {
|
||||
if recip.Title == "" {
|
||||
logger.WithField("docx", recip.DocxPath).Errorln(
|
||||
"Missing title",
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if oldRecip, exists := handler.recipes[recip.Title]; exists {
|
||||
logger.WithFields(log.Fields{
|
||||
"existingPath": oldRecip.DocxPath,
|
||||
"newPath": recip.DocxPath,
|
||||
"title": recip.Title,
|
||||
}).Errorln("Duplicate recipe title")
|
||||
continue
|
||||
}
|
||||
|
||||
handler.recipes[recip.Title] = recip
|
||||
|
||||
logger.WithField("recipeTitle", recip.Title).Debugln("Hashing data")
|
||||
hasher := sha256.New()
|
||||
io.WriteString(hasher, recip.Title)
|
||||
for _, order := range recipe.ValidCategoriesOrder {
|
||||
if info, exists := recip.Info[order]; exists {
|
||||
for _, line := range info {
|
||||
io.WriteString(hasher, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sha256sum := hasher.Sum(nil)
|
||||
|
||||
logger.WithFields(log.Fields{
|
||||
"recipeTitle": recip.Title,
|
||||
"sha256": hex.EncodeToString(sha256sum),
|
||||
}).Debugln("Finished hashing data")
|
||||
|
||||
if oldSha, exists := itemIndex[recip.Title]; !exists || !bytes.Equal(sha256sum, oldSha) {
|
||||
logger.WithFields(log.Fields{
|
||||
"recipeTitle": recip.Title,
|
||||
"docx": recip.DocxPath,
|
||||
}).Infoln("Indexing")
|
||||
|
||||
if exists {
|
||||
handler.idx.Delete(recip.Title)
|
||||
}
|
||||
|
||||
itemIndex[recip.Title] = sha256sum
|
||||
err = handler.idx.Index(recip.Title, recip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Index fail: %s", err.Error())
|
||||
}
|
||||
|
||||
logger.WithField("recipeTitle", recip.Title).Infoln("Indexed")
|
||||
}
|
||||
}
|
||||
|
||||
for recipeTitle := range itemIndex {
|
||||
if _, exists := handler.recipes[recipeTitle]; !exists {
|
||||
logger.WithField("recipeTitle", recipeTitle).Infoln("Removing missing recipe")
|
||||
handler.idx.Delete(recipeTitle)
|
||||
delete(itemIndex, recipeTitle)
|
||||
}
|
||||
}
|
||||
|
||||
err = SaveItemIndex(itemIndex, itemIndexPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("itemIndexPath", itemIndexPath).Warnln(
|
||||
"Failed to update index data",
|
||||
)
|
||||
} else {
|
||||
logger.Infoln("Updated index data")
|
||||
}
|
||||
|
||||
handler.templates, err = NewTemplate(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
// Close handler and free up memory
|
||||
func (h *Handler) Close() error {
|
||||
h.recipes = nil
|
||||
return h.idx.Close()
|
||||
}
|
||||
|
||||
// GetHandlerFuncs in a pattern->func map
|
||||
func (h *Handler) GetHandlerFuncs() map[string]http.HandlerFunc {
|
||||
return map[string]http.HandlerFunc{
|
||||
"/": h.Index,
|
||||
"/search/": h.Search,
|
||||
"/recipes/": h.Recipes,
|
||||
recipePattern: h.Recipe,
|
||||
"/css/mini.css": h.MiniCSS,
|
||||
"/css/main.css": h.MainCSS,
|
||||
imagePattern: h.Images,
|
||||
stockImagePatten: h.StockImages,
|
||||
docxPattern: h.Docx,
|
||||
}
|
||||
}
|
||||
|
||||
// MainCSS outputs the main css file information
|
||||
func (h *Handler) MainCSS(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
|
||||
io.WriteString(w, maincss)
|
||||
}
|
||||
|
||||
// MiniCSS writes the mini css data to writer
|
||||
func (h *Handler) MiniCSS(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
|
||||
io.WriteString(w, minicss)
|
||||
}
|
||||
|
||||
// Index handles index request
|
||||
func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
tmplData := &TemplateData{
|
||||
PageTitle: "Recipe Card",
|
||||
}
|
||||
|
||||
h.templates.ExecuteTemplate(w, "index", tmplData)
|
||||
}
|
||||
|
||||
// Search handles search request
|
||||
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
search := strings.TrimSpace(r.PostFormValue("search"))
|
||||
if search == "" {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
tmplData := &TemplateData{
|
||||
PageTitle: "Recipe Card - Search",
|
||||
SearchValue: search,
|
||||
}
|
||||
|
||||
searchResults, _ := h.idx.Search(bleve.NewSearchRequest(bleve.NewMatchQuery(
|
||||
search,
|
||||
)))
|
||||
|
||||
// try a fuzzy search if matchquery fails
|
||||
if searchResults.Hits.Len() == 0 {
|
||||
searchResults, _ = h.idx.Search(bleve.NewSearchRequest(bleve.NewFuzzyQuery(
|
||||
search,
|
||||
)))
|
||||
}
|
||||
|
||||
for _, hit := range searchResults.Hits {
|
||||
recipe := h.recipes[hit.ID]
|
||||
|
||||
tmplData.Recipes = append(
|
||||
tmplData.Recipes,
|
||||
h.recipeToTemplateRecipe(recipe),
|
||||
)
|
||||
}
|
||||
|
||||
h.templates.ExecuteTemplate(w, "search", tmplData)
|
||||
}
|
||||
|
||||
// Recipes handles recipes page for all recipes
|
||||
func (h *Handler) Recipes(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
tmplData := &TemplateData{
|
||||
PageTitle: "Recipe Card - Recipes",
|
||||
}
|
||||
|
||||
for _, recipe := range h.recipeSlice {
|
||||
tmplData.Recipes = append(
|
||||
tmplData.Recipes,
|
||||
h.recipeToTemplateRecipe(recipe),
|
||||
)
|
||||
}
|
||||
|
||||
h.templates.ExecuteTemplate(w, "recipes", tmplData)
|
||||
}
|
||||
|
||||
// Recipe handles a single recipe page
|
||||
func (h *Handler) Recipe(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
id := strings.TrimPrefix(r.URL.Path, recipePattern)
|
||||
|
||||
if recipe, exists := h.recipes[id]; exists {
|
||||
tmplData := &TemplateData{
|
||||
PageTitle: "Recipe Card - " + id,
|
||||
}
|
||||
|
||||
tmplData.Recipes = append(
|
||||
tmplData.Recipes,
|
||||
h.recipeToTemplateRecipe(recipe),
|
||||
)
|
||||
|
||||
h.templates.ExecuteTemplate(w, "recipe", tmplData)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// StockImages handles all stock image requests
|
||||
func (h *Handler) StockImages(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
|
||||
id := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, ".jpg"), stockImagePatten)
|
||||
if recipe, exists := h.recipes[id]; exists {
|
||||
w.Write(recipe.Image)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Docx handles all docx download requests
|
||||
func (h *Handler) Docx(w http.ResponseWriter, r *http.Request) {
|
||||
lowerPath := strings.ToLower(r.URL.Path)
|
||||
|
||||
if !strings.HasSuffix(lowerPath, "docx") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(h.urlToPath(r.URL.Path, docxPattern))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
||||
io.Copy(w, file)
|
||||
}
|
||||
|
||||
// Images handles all image requests
|
||||
func (h *Handler) Images(w http.ResponseWriter, r *http.Request) {
|
||||
lowerPath := strings.ToLower(r.URL.Path)
|
||||
|
||||
if !strings.HasSuffix(lowerPath, "jpg") && !strings.HasSuffix(lowerPath, "jpeg") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(h.urlToPath(r.URL.Path, imagePattern))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
io.Copy(w, file)
|
||||
}
|
||||
|
||||
func (h *Handler) pathToURL(filePath, pattern string) (string, error) {
|
||||
path, err := filepath.Rel(h.recipePath, filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
urlPath := pattern[:len(pattern)-1]
|
||||
for _, pathPart := range strings.Split(path, string(filepath.Separator)) {
|
||||
if pathPart == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
urlPath += "/" + url.PathEscape(pathPart)
|
||||
}
|
||||
|
||||
return urlPath, nil
|
||||
}
|
||||
|
||||
func (h *Handler) urlToPath(url, pattern string) string {
|
||||
path := filepath.Join(
|
||||
h.recipePath,
|
||||
strings.Replace(strings.TrimPrefix(url, pattern), "/", string(filepath.Separator), -1),
|
||||
)
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
log.WithFields(log.Fields{
|
||||
"url": url,
|
||||
"pattern": pattern,
|
||||
"path": path,
|
||||
}).Errorln("Must only receive absolute path")
|
||||
return ""
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// recipeToTemplateRecipe converts a recipe.Recipe to a TemplateRecipe
|
||||
func (h *Handler) recipeToTemplateRecipe(rec *recipe.Recipe) *TemplateRecipe {
|
||||
tmplRecipe := &TemplateRecipe{
|
||||
ID: rec.Title,
|
||||
URL: "/recipe/" + url.PathEscape(rec.Title),
|
||||
StockImage: stockImagePatten + url.PathEscape(rec.Title+".jpg"),
|
||||
}
|
||||
|
||||
docxURL, err := h.pathToURL(rec.DocxPath, docxPattern)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("docxPath", rec.DocxPath).Warnln(
|
||||
"Failed to get docx url",
|
||||
)
|
||||
} else {
|
||||
tmplRecipe.DocxURL = docxURL
|
||||
}
|
||||
|
||||
for _, imagePath := range rec.ScanPaths {
|
||||
urlPath, err := h.pathToURL(imagePath, imagePattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tmplRecipe.Images = append(
|
||||
tmplRecipe.Images,
|
||||
urlPath,
|
||||
)
|
||||
}
|
||||
|
||||
for _, category := range recipe.ValidCategoriesOrder {
|
||||
if info, exists := rec.Info[category]; exists {
|
||||
description := ""
|
||||
for _, infoLine := range info {
|
||||
description += "<p>" + html.EscapeString(infoLine) + "</p>"
|
||||
}
|
||||
|
||||
tmplRecipe.Description += template.HTML(fmt.Sprintf(
|
||||
"<h3>%s</h3>%s",
|
||||
html.EscapeString(category),
|
||||
description,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return tmplRecipe
|
||||
}
|
144
main.go
Normal file
144
main.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
open "github.com/skratchdot/open-golang/open"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Mux is the http servemux for this server
|
||||
type Mux struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// NewMux creates a new mux instance
|
||||
func NewMux() *Mux {
|
||||
return &Mux{
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle registers the handler for the given pattern
|
||||
func (mux *Mux) Handle(pattern string, handler http.Handler) {
|
||||
mux.mux.Handle(pattern, handler)
|
||||
}
|
||||
|
||||
// HandleFunc registers the handler func for the given pattern
|
||||
func (mux *Mux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
|
||||
mux.mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// ServeHTTP dispatches the request to the handler whose pattern most closely matches the request URL
|
||||
func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.WithFields(log.Fields{
|
||||
"remoteAddr": r.RemoteAddr,
|
||||
"userAgent": r.UserAgent(),
|
||||
"requestURI": r.RequestURI,
|
||||
"method": r.Method,
|
||||
}).Debug()
|
||||
mux.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// only output >= info by default
|
||||
log.SetLevel(log.InfoLevel)
|
||||
|
||||
// get good timestamps for logs
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
})
|
||||
|
||||
// get the path relative to the executble
|
||||
recipePath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Failed to get executable path")
|
||||
recipePath = ""
|
||||
}
|
||||
|
||||
debug := false
|
||||
listenAddr := "127.0.0.1"
|
||||
listenPort := uint16(0)
|
||||
indexPath := filepath.Join(path.Dir(recipePath), "search_idx")
|
||||
recipePath = filepath.Join(path.Dir(recipePath), "Recipes")
|
||||
recipePath, err = filepath.Abs(recipePath)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Failed to get absolute path for default recipe path")
|
||||
recipePath = ""
|
||||
}
|
||||
indexPath, err = filepath.Abs(indexPath)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Failed to get absolute path for default index path")
|
||||
indexPath = ""
|
||||
}
|
||||
|
||||
flag.StringVarP(&listenAddr, "host", "h", listenAddr, "HTTP listen address")
|
||||
flag.Uint16VarP(&listenPort, "port", "p", listenPort, "HTTP listen port")
|
||||
flag.StringVarP(&recipePath, "recipes", "r", recipePath, "Path to recipes")
|
||||
flag.StringVarP(&indexPath, "index", "i", indexPath, "Path for search index")
|
||||
flag.BoolVarP(&debug, "debug", "d", debug, "Enable debug mode")
|
||||
flag.Parse()
|
||||
|
||||
if debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"host": listenAddr,
|
||||
"port": listenPort,
|
||||
"recipes": recipePath,
|
||||
"index": indexPath,
|
||||
"debug": debug,
|
||||
}).Debugln("Options received")
|
||||
|
||||
log.Debugln("Creating new handler")
|
||||
handler, err := NewHandler(recipePath, indexPath, log.StandardLogger())
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Failed to create new handler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Debugln("Successfully created new handler")
|
||||
|
||||
defer handler.Close()
|
||||
|
||||
log.Debugln("Creating TCP listening port")
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", listenAddr, listenPort))
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Failed to listen on new TCP port")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
mux := NewMux()
|
||||
|
||||
log.Debugln("Associating handler funcs with http server")
|
||||
for pattern, handlerFunc := range handler.GetHandlerFuncs() {
|
||||
mux.HandleFunc(pattern, handlerFunc)
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infoln("Waiting before opening web browser")
|
||||
time.Sleep(time.Second)
|
||||
url := "http://" + listener.Addr().String()
|
||||
err = open.Run(url)
|
||||
if err == nil {
|
||||
log.Infoln("Opened", url, "in your web browser")
|
||||
} else {
|
||||
log.Infoln("Open", url, "in your web browser")
|
||||
}
|
||||
}()
|
||||
|
||||
err = http.Serve(listener, mux)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatalln("HTTP server died")
|
||||
}
|
||||
}
|
186
recipe/recipe.go
Normal file
186
recipe/recipe.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tblyler/goatomic"
|
||||
"github.com/tblyler/recipe-card/doc"
|
||||
)
|
||||
|
||||
// ValidCategoriesOrder defines keys of info in order of importance
|
||||
var ValidCategoriesOrder = []string{
|
||||
"serves",
|
||||
"oven temperature",
|
||||
"ingredients",
|
||||
"preparation",
|
||||
"tips",
|
||||
}
|
||||
|
||||
// validCategories defines keys of Info
|
||||
var validCategories = map[string]bool{
|
||||
"oven temperature": true,
|
||||
"serves": true,
|
||||
"ingredients": true,
|
||||
"preparation": true,
|
||||
"tips": true,
|
||||
}
|
||||
|
||||
// Recipe stores information regarding a specific recipe
|
||||
type Recipe struct {
|
||||
Title string `json:"title"`
|
||||
Info map[string][]string `json:"info"`
|
||||
// FIXME support non-docx
|
||||
DocxPath string `json:"docx_path"`
|
||||
ScanPaths []string `json:"scan_paths"`
|
||||
Image []byte
|
||||
}
|
||||
|
||||
// Summary outputs a nice summary of Info
|
||||
func (r *Recipe) Summary() (output string) {
|
||||
for _, category := range ValidCategoriesOrder {
|
||||
if info, exists := r.Info[category]; exists {
|
||||
if output != "" {
|
||||
// add an extra newline between categories
|
||||
output += "\n"
|
||||
}
|
||||
|
||||
output += category + "\n" + strings.Join(info, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParseFiles for the recipe
|
||||
func (r *Recipe) ParseFiles() error {
|
||||
dir := filepath.Dir(r.DocxPath)
|
||||
|
||||
// get a list of recipe scans
|
||||
infos, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.ToLower(info.Name())
|
||||
// FIXME support non-jpeg
|
||||
if !strings.HasSuffix(name, ".jpeg") && !strings.HasSuffix(name, ".jpg") {
|
||||
continue
|
||||
}
|
||||
|
||||
r.ScanPaths = append(r.ScanPaths, filepath.Join(dir, info.Name()))
|
||||
}
|
||||
|
||||
sort.Strings(r.ScanPaths)
|
||||
|
||||
file, err := os.Open(r.DocxPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docx, err := doc.NewDocx(file, stat.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Image = docx.Image
|
||||
|
||||
lines, err := docx.Text()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Info = make(map[string][]string)
|
||||
|
||||
titleIsNext := false
|
||||
currentGroup := ""
|
||||
for _, line := range lines {
|
||||
if r.Title == "" {
|
||||
if titleIsNext {
|
||||
r.Title = line
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(line), "recipe") {
|
||||
titleIsNext = true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
lowerLine := strings.ToLower(strings.Replace(line, ":", "", -1))
|
||||
if _, exists := validCategories[lowerLine]; exists {
|
||||
currentGroup = lowerLine
|
||||
continue
|
||||
}
|
||||
|
||||
// make sure a current group is set
|
||||
if currentGroup == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Info[currentGroup] = append(r.Info[currentGroup], line)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecipesFromPath generates Recipe instances from a path
|
||||
func RecipesFromPath(dirPath string) (recipes []*Recipe, err error) {
|
||||
// get the absolute path of the directory and clean it
|
||||
dirPath, err = filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return nil, fmt.Errorf("Not a directory %s", dirPath)
|
||||
}
|
||||
|
||||
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||
// skip directories and non-docx files
|
||||
// FIXME support non-docx
|
||||
if info.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".docx") {
|
||||
return nil
|
||||
}
|
||||
|
||||
recipes = append(recipes, &Recipe{
|
||||
DocxPath: path,
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
wg := goatomic.WorkerGroup{}
|
||||
for _, recipe := range recipes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(recipe *Recipe) {
|
||||
recipe.ParseFiles()
|
||||
wg.Done()
|
||||
}(recipe)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return
|
||||
}
|
309
templates.go
Normal file
309
templates.go
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue