Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // pdfxform is a server that rasterizes PDF documents into PNG | |
| 2 package main | |
| 3 | |
| 4 import ( | |
| 5 "bytes" | |
| 6 "crypto/md5" | |
| 7 "encoding/hex" | |
| 8 "encoding/json" | |
| 9 "flag" | |
| 10 "fmt" | |
| 11 "io" | |
| 12 "io/ioutil" | |
| 13 "net/http" | |
| 14 "os" | |
| 15 "os/user" | |
| 16 "path" | |
| 17 "path/filepath" | |
| 18 "strings" | |
| 19 "time" | |
| 20 | |
| 21 "github.com/skia-dev/glog" | |
| 22 "go.skia.org/infra/go/auth" | |
| 23 "go.skia.org/infra/go/common" | |
| 24 "go.skia.org/infra/go/gs" | |
| 25 "go.skia.org/infra/go/pdf" | |
| 26 "go.skia.org/infra/go/util" | |
| 27 "go.skia.org/infra/perf/go/goldingester" | |
| 28 "google.golang.org/api/storage/v1" | |
| 29 ) | |
| 30 | |
| 31 //////////////////////////////////////////////////////////////////////////////// | |
| 32 | |
| 33 const ( | |
| 34 PNG_EXT = "png" | |
| 35 PDF_EXT = "pdf" | |
| 36 ) | |
| 37 | |
| 38 //////////////////////////////////////////////////////////////////////////////// | |
| 39 | |
| 40 // md5OfFile calculates the MD5 checksum of a file. | |
| 41 func md5OfFile(path string) (string, error) { | |
| 42 md5 := md5.New() | |
| 43 f, err := os.Open(path) | |
| 44 if err != nil { | |
| 45 return "", err | |
| 46 } | |
| 47 defer util.Close(f) | |
| 48 if _, err = io.Copy(md5, f); err != nil { | |
| 49 return "", err | |
| 50 } | |
| 51 return hex.EncodeToString(md5.Sum(nil)), nil | |
| 52 } | |
| 53 | |
| 54 // removeIfExists is like util.Remove, but logs no error if the file does not ex ist. | |
| 55 func removeIfExists(path string) { | |
| 56 if err := os.Remove(path); err != nil { | |
| 57 if !os.IsNotExist(err) { | |
| 58 glog.Errorf("Failed to Remove(%s): %v", path, err) | |
| 59 } | |
| 60 } | |
| 61 } | |
| 62 | |
| 63 // isPDF returns true if the path appears to point to a PDF file. | |
| 64 func isPDF(path string) bool { | |
| 65 f, err := os.Open(path) | |
| 66 if err != nil { | |
| 67 return false | |
| 68 } | |
| 69 defer util.Close(f) | |
| 70 var buffer [4]byte | |
| 71 if n, err := f.Read(buffer[:]); n != 4 || err != nil { | |
| 72 return false | |
| 73 } | |
| 74 var magic = [4]byte{'%', 'P', 'D', 'F'} | |
| 75 return bytes.Equal(magic[:], buffer[:]) | |
| 76 } | |
| 77 | |
| 78 // writeTo opens a file and dumps the contents of the reader into it. | |
| 79 func writeTo(path string, reader *io.ReadCloser) error { | |
| 80 defer util.Close(*reader) | |
| 81 file, err := os.Create(path) | |
| 82 if err == nil { | |
| 83 _, err = io.Copy(file, *reader) | |
| 84 } | |
| 85 return err | |
| 86 } | |
| 87 | |
| 88 //////////////////////////////////////////////////////////////////////////////// | |
| 89 | |
| 90 // storageClient struct is used for uploading to cloud storage | |
| 91 type storageClient struct { | |
| 92 httpClient *http.Client | |
| 93 storageService *storage.Service | |
| 94 } | |
| 95 | |
| 96 // getClient returns an authorized storage.Service and the | |
| 97 // corresponding http.Client; if anything goes wrong, it logs a fatal | |
| 98 // error. | |
| 99 func getClient() (storageClient, error) { | |
| 100 var client *http.Client | |
| 101 var err error | |
| 102 if *local { | |
| 103 client, err = auth.RunFlow(auth.OAuthConfig(*oauthCacheFile, aut h.SCOPE_FULL_CONTROL)) | |
| 104 // TODO(stephana): Replace auth.RunFlow with auth.NewClient | |
| 105 // client, err = auth.NewClient(true, *oauthCacheFile, auth.SCOP E_FULL_CONTROL, auth.SCOPE_GCE) | |
| 106 } else { | |
| 107 client = auth.GCEServiceAccountClient(&http.Transport{Dial: util .DialTimeout}) | |
| 108 } | |
| 109 if err != nil { | |
| 110 return storageClient{}, err | |
| 111 } | |
| 112 gsService, err := storage.New(client) | |
| 113 if err != nil { | |
| 114 return storageClient{}, err | |
| 115 } | |
| 116 return storageClient{httpClient: client, storageService: gsService}, nil | |
| 117 } | |
| 118 | |
| 119 // gsFetch fetch the object's data from google storage | |
| 120 func gsFetch(object *storage.Object, sc storageClient) (io.ReadCloser, int64, er ror) { | |
| 121 request, err := gs.RequestForStorageURL(object.MediaLink) | |
| 122 if err != nil { | |
| 123 return nil, -1, err | |
| 124 } | |
| 125 resp, err := sc.httpClient.Do(request) | |
| 126 if err != nil { | |
| 127 return nil, -1, err | |
| 128 } | |
| 129 if resp.StatusCode != 200 { | |
| 130 resp.Body.Close() | |
| 131 return nil, -1, fmt.Errorf("Failed to retrieve: %s %d %s", objec t.MediaLink, resp.StatusCode, resp.Status) | |
| 132 } | |
| 133 return resp.Body, resp.ContentLength, nil | |
| 134 } | |
| 135 | |
| 136 // uploadFile uploads the specified file to the remote dir in Google | |
| 137 // Storage. It also sets the appropriate ACLs on the uploaded file. | |
| 138 // If the file already exists on the server, do nothing. | |
| 139 func uploadFile(sc storageClient, input io.Reader, storageBucket, storagePath, a ccessControlEntity string) (bool, error) { | |
| 140 obj, _ := sc.storageService.Objects.Get(storageBucket, storagePath).Do() | |
| 141 if obj != nil { | |
| 142 return false, nil // noclobber | |
| 143 } | |
| 144 fullPath := fmt.Sprintf("gs://%s/%s", storageBucket, storagePath) | |
| 145 object := &storage.Object{Name: storagePath} | |
| 146 if _, err := sc.storageService.Objects.Insert(storageBucket, object).Med ia(input).Do(); err != nil { | |
| 147 return false, fmt.Errorf("Objects.Insert(%s) failed: %s", fullPa th, err) | |
| 148 } | |
| 149 objectAcl := &storage.ObjectAccessControl{ | |
| 150 Bucket: storageBucket, Entity: accessControlEntity, Object: stor agePath, Role: "READER", | |
| 151 } | |
| 152 if _, err := sc.storageService.ObjectAccessControls.Insert(storageBucket , storagePath, objectAcl).Do(); err != nil { | |
| 153 return false, fmt.Errorf("Could not update ACL of %s: %s", fullP ath, err) | |
| 154 } | |
| 155 return true, nil | |
| 156 } | |
| 157 | |
| 158 //////////////////////////////////////////////////////////////////////////////// | |
| 159 | |
| 160 var ( | |
| 161 local = flag.Bool("local", false, "Set to true if not r unning in prod") | |
| 162 oauthCacheFile = flag.String("oauth_cache_file", "oauth_cache.da t", "Path to look for and store an OAuth token") | |
| 163 dataDir = flag.String("data_dir", "", "Directory to store data in.") | |
| 164 failureImage = flag.String("failure_image", "", "Location of a PNG image; must be set") | |
| 165 storageBucket = flag.String("storage_bucket", "chromium-skia-gm ", "The bucket for json, pdf, and png files") | |
| 166 storageJsonDirectory = flag.String("storage_json_directory", "dm-json- v1", "The directory on bucket for json files.") | |
| 167 storageImagesDirectory = flag.String("storage_images_directory", "dm-ima ges-v1", "The directory on bucket for png and pdf files.") | |
| 168 accessControlEntity = flag.String("access_control_entity", "domain-go ogle.com", "The entity that has permissions to manage the bucket") | |
| 169 graphiteServer = flag.String("graphite_server", "skia-monitoring :2003", "Where the Graphite metrics ingestion server is running") | |
| 170 ) | |
| 171 | |
| 172 // The pdfXformer struct holds state | |
| 173 type pdfXformer struct { | |
| 174 client storageClient | |
| 175 rasterizers []pdf.Rasterizer | |
| 176 results map[string]map[int]string | |
| 177 counter int | |
| 178 identifier string | |
| 179 errorImageMd5 string | |
| 180 } | |
| 181 | |
| 182 // rasterizeOnce applies a single rastetizer to the given pdf file. | |
| 183 // If the rasterizer fails, use the errorImage. If everything | |
| 184 // succeeds, upload the PNG. | |
| 185 func (xformer *pdfXformer) rasterizeOnce(pdfPath string, rasterizerIndex int) (s tring, error) { | |
| 186 rasterizer := xformer.rasterizers[rasterizerIndex] | |
| 187 tempdir := filepath.Dir(pdfPath) | |
| 188 pngPath := path.Join(tempdir, fmt.Sprintf("%s.%s", rasterizer.String(), PNG_EXT)) | |
| 189 defer removeIfExists(pngPath) | |
| 190 glog.Infof("> > > > rasterizing with %s", rasterizer) | |
| 191 err := rasterizer.Rasterize(pdfPath, pngPath) | |
| 192 if err != nil { | |
| 193 glog.Warningf("rasterizing %s with %s failed: %s", filepath.Base (pdfPath), rasterizer.String(), err) | |
| 194 return xformer.errorImageMd5, nil | |
| 195 } | |
| 196 md5, err := md5OfFile(pngPath) | |
| 197 if err != nil { | |
| 198 return "", err | |
| 199 } | |
| 200 f, err := os.Open(pngPath) | |
| 201 if err != nil { | |
| 202 return "", err | |
| 203 } | |
| 204 defer util.Close(f) | |
| 205 pngUploadPath := fmt.Sprintf("%s/%s.%s", *storageImagesDirectory, md5, P NG_EXT) | |
| 206 didUpload, err := uploadFile(xformer.client, f, *storageBucket, pngUploa dPath, *accessControlEntity) | |
| 207 if err != nil { | |
| 208 return "", err | |
| 209 } | |
| 210 if didUpload { | |
| 211 glog.Infof("> > > > uploaded %s", pngUploadPath) | |
| 212 } | |
| 213 return md5, nil | |
| 214 } | |
| 215 | |
| 216 // makeTmpDir returns a nicely-named directory for temp files in $TMPDIR | |
| 217 func (xformer *pdfXformer) makeTmpDir() string { | |
|
stephana
2015/07/09 17:18:54
This should return the error IMO.
hal.canary
2015/07/09 17:37:38
Done.
| |
| 218 if xformer.identifier == "" { | |
| 219 var host, userName string | |
| 220 if h, err := os.Hostname(); err == nil { | |
| 221 host = h | |
| 222 if i := strings.Index(host, "."); i >= 0 { | |
| 223 host = host[:i] | |
| 224 } | |
| 225 } | |
| 226 if currentUser, err := user.Current(); err == nil { | |
| 227 userName = currentUser.Username | |
| 228 } | |
| 229 userName = strings.Replace(userName, `\`, "_", -1) | |
| 230 xformer.identifier = fmt.Sprintf("%s.%s.%s.tmp.%d.", filepath.Ba se(os.Args[0]), host, userName, os.Getpid()) | |
| 231 } | |
| 232 tempdir, err := ioutil.TempDir(*dataDir, xformer.identifier) | |
| 233 if err != nil { | |
| 234 glog.Errorf("error returned from ioutil.TempDir: %s", err) | |
| 235 return "" | |
| 236 } | |
| 237 return tempdir | |
| 238 } | |
| 239 | |
| 240 func newResult(key map[string]string, rasterizerName, digest string) goldingeste r.Result { | |
| 241 keyCopy := map[string]string{} | |
| 242 for k, v := range key { | |
| 243 keyCopy[k] = v | |
| 244 } | |
| 245 keyCopy["rasterizer"] = rasterizerName | |
| 246 options := map[string]string{"ext": PNG_EXT} | |
| 247 return goldingester.Result{Key: keyCopy, Digest: digest, Options: option s} | |
| 248 } | |
| 249 | |
| 250 // processResult rasterizes a single PDF result and returns a set of new results . | |
| 251 func (xformer *pdfXformer) processResult(res goldingester.Result) []goldingester .Result { | |
|
stephana
2015/07/09 17:18:54
This should return []*goldingester.Result
hal.canary
2015/07/09 17:37:38
Done.
| |
| 252 rasterizedResults := []goldingester.Result{} | |
| 253 resultMap, found := xformer.results[res.Digest] | |
| 254 if found { | |
| 255 // Skip rasterizion steps: big win. | |
| 256 for index, rasterizer := range xformer.rasterizers { | |
| 257 digest, ok := resultMap[index] | |
| 258 if ok { | |
| 259 rasterizedResults = append(rasterizedResults, | |
| 260 newResult(res.Key, rasterizer.String(), digest)) | |
| 261 } else { | |
| 262 glog.Errorf("missing rasterizer %s on %s", raste rizer.String(), res.Digest) | |
| 263 } | |
| 264 } | |
| 265 return rasterizedResults | |
| 266 } | |
| 267 | |
| 268 tempdir := xformer.makeTmpDir() | |
| 269 if tempdir == "" { | |
|
stephana
2015/07/09 17:18:54
Test for an error here instead of an empty string
hal.canary
2015/07/09 17:37:39
Done.
| |
| 270 return rasterizedResults | |
| 271 } | |
| 272 defer util.RemoveAll(tempdir) | |
| 273 pdfPath := path.Join(tempdir, fmt.Sprintf("%s.pdf", res.Digest)) | |
| 274 objectName := fmt.Sprintf("%s/%s.pdf", *storageImagesDirectory, res.Dige st) | |
| 275 storageURL := fmt.Sprintf("gs://%s/%s", *storageBucket, objectName) | |
| 276 object, err := xformer.client.storageService.Objects.Get(*storageBucket, objectName).Do() | |
| 277 if err != nil { | |
| 278 glog.Errorf("unable to find %s: %s", storageURL, err) | |
| 279 return []goldingester.Result{} | |
| 280 } | |
| 281 pdfData, _, err := gsFetch(object, xformer.client) | |
| 282 if err != nil { | |
| 283 glog.Errorf("unable to retrieve %s: %s", storageURL, err) | |
| 284 return []goldingester.Result{} | |
| 285 } | |
| 286 writeTo(pdfPath, &pdfData) | |
| 287 if !isPDF(pdfPath) { | |
| 288 glog.Errorf("%s is not a PDF", objectName) | |
| 289 return []goldingester.Result{} | |
| 290 } | |
| 291 resultMap = map[int]string{} | |
| 292 for index, rasterizer := range xformer.rasterizers { | |
| 293 digest, err := xformer.rasterizeOnce(pdfPath, index) | |
| 294 if err != nil { | |
| 295 glog.Errorf("rasterizer %s failed on %s.pdf: %s", raster izer, res.Digest, err) | |
| 296 continue | |
| 297 } | |
| 298 rasterizedResults = append(rasterizedResults, | |
| 299 newResult(res.Key, rasterizer.String(), digest)) | |
| 300 resultMap[index] = digest | |
| 301 } | |
| 302 xformer.results[res.Digest] = resultMap | |
| 303 return rasterizedResults | |
| 304 } | |
| 305 | |
| 306 // processJsonFile reads a json file and produces a new json file | |
| 307 // with rasterized results. | |
| 308 func (xformer *pdfXformer) processJsonFile(jsonFileObject *storage.Object) { | |
| 309 jsonURL := fmt.Sprintf("gs://%s/%s", *storageBucket, jsonFileObject.Name ) | |
| 310 if jsonFileObject.Metadata["rasterized"] == "true" { | |
| 311 glog.Infof("> > skipping %s (already processed) {%d}", jsonURL, xformer.counter) | |
| 312 return | |
| 313 } | |
| 314 body, length, err := gsFetch(jsonFileObject, xformer.client) | |
| 315 if err != nil { | |
| 316 glog.Errorf("Failed to fetch %s", jsonURL) | |
| 317 return | |
| 318 } | |
| 319 if 0 == length { | |
| 320 util.Close(body) | |
| 321 glog.Infof("> > skipping %s (empty file) {%d}", jsonURL, xformer .counter) | |
| 322 return | |
| 323 } | |
| 324 dmstruct := goldingester.DMResults{} | |
| 325 err = json.NewDecoder(body).Decode(&dmstruct) | |
| 326 util.Close(body) | |
| 327 if err != nil { | |
| 328 glog.Errorf("Failed to parse %s", jsonURL) | |
| 329 return | |
| 330 } | |
| 331 countPdfResults := 0 | |
| 332 for _, res := range dmstruct.Results { | |
| 333 if res.Options["ext"] == PDF_EXT { | |
| 334 countPdfResults++ | |
| 335 } | |
| 336 } | |
| 337 if 0 == countPdfResults { | |
| 338 glog.Infof("> > 0 PDFs found %s {%d}", jsonURL, xformer.counter) | |
| 339 xformer.setRasterized(jsonFileObject) | |
| 340 return | |
| 341 } | |
| 342 | |
| 343 glog.Infof("> > processing %d pdfs of %d results {%d}", countPdfResults, len(dmstruct.Results), xformer.counter) | |
| 344 rasterizedResults := []*goldingester.Result{} | |
| 345 i := 0 | |
| 346 for _, res := range dmstruct.Results { | |
| 347 if res.Options["ext"] == PDF_EXT { | |
| 348 i++ | |
| 349 glog.Infof("> > > processing %s.pdf [%d/%d] {%d}", res.D igest, i, countPdfResults, xformer.counter) | |
| 350 for _, rasterizedResult := range xformer.processResult(* res) { | |
|
stephana
2015/07/09 17:18:54
If you make the changes to processResult suggested
hal.canary
2015/07/09 17:37:38
Done.
| |
| 351 rasterizedResults = append(rasterizedResults, &r asterizedResult) | |
| 352 } | |
| 353 } | |
| 354 } | |
| 355 newDMStruct := goldingester.DMResults{ | |
| 356 BuildNumber: dmstruct.BuildNumber, | |
| 357 GitHash: dmstruct.GitHash, | |
| 358 Key: dmstruct.Key, | |
| 359 Results: rasterizedResults, | |
| 360 } | |
| 361 newJson, err := json.Marshal(newDMStruct) | |
| 362 if err != nil { | |
| 363 glog.Errorf("Unexpected json.Marshal error: %s", err) | |
| 364 return | |
| 365 } | |
| 366 | |
| 367 now := time.Now() | |
| 368 // Change the date; leave most of the rest of the path components. | |
| 369 jsonPathComponents := strings.Split(jsonFileObject.Name, "/") // []strin g | |
| 370 if len(jsonPathComponents) < 4 { | |
| 371 fmt.Errorf("unexpected number of path components %q", jsonPathCo mponents) | |
| 372 return | |
| 373 } | |
| 374 jsonPathComponents = jsonPathComponents[len(jsonPathComponents)-4:] | |
| 375 jsonPathComponents[1] += "-pdfxformer" | |
| 376 jsonUploadPath := fmt.Sprintf("%s/%d/%02d/%02d/%02d/%s", | |
| 377 *storageJsonDirectory, | |
| 378 now.Year(), | |
| 379 int(now.Month()), | |
| 380 now.Day(), | |
| 381 now.Hour(), | |
| 382 strings.Join(jsonPathComponents, "/")) | |
| 383 | |
| 384 _, err = uploadFile(xformer.client, bytes.NewReader(newJson), *storageBu cket, jsonUploadPath, *accessControlEntity) | |
| 385 glog.Infof("> > wrote gs://%s/%s", *storageBucket, jsonUploadPath) | |
| 386 newJsonFileObject, err := xformer.client.storageService.Objects.Get(*sto rageBucket, jsonUploadPath).Do() | |
| 387 if err != nil { | |
| 388 glog.Errorf("Failed to find %s: %s", jsonUploadPath, err) | |
| 389 } else { | |
| 390 xformer.setRasterized(newJsonFileObject) | |
| 391 } | |
| 392 xformer.setRasterized(jsonFileObject) | |
| 393 } | |
| 394 | |
| 395 // setRasterized sets the rasterized metadata flag of the given storage.Object | |
| 396 func (xformer *pdfXformer) setRasterized(jsonFileObject *storage.Object) { | |
| 397 if nil == jsonFileObject.Metadata { | |
| 398 jsonFileObject.Metadata = map[string]string{} | |
| 399 } | |
| 400 jsonFileObject.Metadata["rasterized"] = "true" | |
| 401 _, err := xformer.client.storageService.Objects.Patch(*storageBucket, js onFileObject.Name, jsonFileObject).Do() | |
| 402 if err != nil { | |
| 403 glog.Errorf("Failed to update metadata of %s: %s", jsonFileObjec t.Name, err) | |
| 404 } else { | |
| 405 glog.Infof("> > Updated metadata of %s", jsonFileObject.Name) | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 // processTimeRange calls gs.GetLatestGSDirs to get a list of | |
| 410 func (xformer *pdfXformer) processTimeRange(start time.Time, end time.Time) { | |
| 411 glog.Infof("Processing time range: (%s, %s)", start.Truncate(time.Second ), end.Truncate(time.Second)) | |
| 412 for _, dir := range gs.GetLatestGSDirs(start.Unix(), end.Unix(), *storag eJsonDirectory) { | |
| 413 glog.Infof("> Reading gs://%s/%s\n", *storageBucket, dir) | |
| 414 requestedObjects := xformer.client.storageService.Objects.List(* storageBucket).Prefix(dir).Fields( | |
| 415 "nextPageToken", "items/updated", "items/md5Hash", "item s/mediaLink", "items/name", "items/metadata") | |
| 416 for requestedObjects != nil { | |
| 417 responseObjects, err := requestedObjects.Do() | |
| 418 if err != nil { | |
| 419 glog.Errorf("request %#v failed: %s", requestedO bjects, err) | |
| 420 } else { | |
| 421 for _, jsonObject := range responseObjects.Items { | |
| 422 xformer.counter++ | |
| 423 glog.Infof("> > Processing object: gs:/ /%s/%s {%d}", *storageBucket, jsonObject.Name, xformer.counter) | |
| 424 xformer.processJsonFile(jsonObject) | |
| 425 } | |
| 426 } | |
| 427 if len(responseObjects.NextPageToken) > 0 { | |
| 428 requestedObjects.PageToken(responseObjects.NextP ageToken) | |
| 429 } else { | |
| 430 requestedObjects = nil | |
| 431 } | |
| 432 } | |
| 433 } | |
| 434 glog.Infof("finished time range.") | |
| 435 } | |
| 436 | |
| 437 // uploadErrorImage should be run once to verify that the image is there | |
| 438 func (xformer *pdfXformer) uploadErrorImage(path string) error { | |
| 439 if "" == path { | |
| 440 glog.Fatalf("Missing --path argument") | |
| 441 } | |
| 442 errorImageMd5, err := md5OfFile(path) | |
| 443 if err != nil { | |
| 444 glog.Fatalf("Bad --path argument") | |
| 445 } | |
| 446 errorImageFileReader, err := os.Open(path) | |
| 447 if err != nil { | |
| 448 return err | |
| 449 } | |
| 450 defer util.Close(errorImageFileReader) | |
| 451 errorImagePath := fmt.Sprintf("%s/%s.png", *storageImagesDirectory, erro rImageMd5) | |
| 452 _, err = uploadFile(xformer.client, errorImageFileReader, *storageBucket , errorImagePath, *accessControlEntity) | |
| 453 if err != nil { | |
| 454 return err | |
| 455 } | |
| 456 xformer.errorImageMd5 = errorImageMd5 | |
| 457 return nil | |
| 458 } | |
| 459 | |
| 460 func main() { | |
| 461 flag.Parse() | |
| 462 common.InitWithMetrics("pdfxform", graphiteServer) | |
| 463 | |
| 464 client, err := getClient() | |
| 465 if err != nil { | |
| 466 glog.Fatal(err) | |
| 467 } | |
| 468 xformer := pdfXformer{ | |
| 469 client: client, | |
| 470 results: map[string]map[int]string{}, | |
| 471 } | |
| 472 | |
| 473 err = xformer.uploadErrorImage(*failureImage) | |
| 474 if err != nil { | |
| 475 // If we can't upload this, we can't upload anything. | |
| 476 glog.Fatalf("Filed to upload error image: %s", err) | |
| 477 } | |
| 478 | |
| 479 for _, rasterizer := range []pdf.Rasterizer{pdf.Pdfium{}, pdf.Poppler{}} { | |
| 480 if rasterizer.Enabled() { | |
| 481 xformer.rasterizers = append(xformer.rasterizers, raster izer) | |
| 482 } else { | |
| 483 glog.Infof("rasterizer %s is disabled", rasterizer.Strin g()) | |
| 484 } | |
| 485 } | |
| 486 if len(xformer.rasterizers) == 0 { | |
| 487 glog.Fatalf("no rasterizers found") | |
| 488 } | |
| 489 | |
| 490 end := time.Now() | |
| 491 start := end.Add(-172 * time.Hour) | |
| 492 xformer.processTimeRange(start, end) | |
| 493 glog.Flush() // Flush before waiting for next tick; it may be a while. | |
| 494 for _ = range time.Tick(time.Minute) { | |
| 495 start, end = end, time.Now() | |
| 496 xformer.processTimeRange(start, end) | |
| 497 glog.Flush() | |
| 498 } | |
| 499 } | |
| OLD | NEW |