App Engine Java SDK version 1.7.0
[gae.git] / java / src / main / com / google / appengine / tools / admin / AppVersionUpload.java
blob948c79f85032dccbf79faa6ad57b91a8ebca4274
1 // Copyright 2009 Google Inc. All rights reserved.
3 package com.google.appengine.tools.admin;
5 import com.google.appengine.tools.admin.GenericApplication.ErrorHandler;
6 import com.google.appengine.tools.util.FileIterator;
7 import com.google.common.annotations.VisibleForTesting;
8 import com.google.common.base.Join;
10 import java.io.File;
11 import java.io.FileInputStream;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.net.HttpURLConnection;
15 import java.security.MessageDigest;
16 import java.security.NoSuchAlgorithmException;
17 import java.text.MessageFormat;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.TreeMap;
26 import java.util.concurrent.Callable;
27 import java.util.logging.Logger;
28 import java.util.regex.Pattern;
30 /**
31 * Uploads a new appversion to the hosting service.
34 public class AppVersionUpload {
35 /**
36 * Don't try to precompile more than this number of files in one request.
38 private static final int MAX_FILES_PER_PRECOMPILE = 50;
40 protected ServerConnection connection;
41 protected GenericApplication app;
42 protected final String backend;
43 private final Logger logger = Logger.getLogger(AppVersionUpload.class.getName());
44 private boolean inTransaction = false;
45 private Map<String, FileInfo> files = new HashMap<String, FileInfo>();
46 private boolean deployed = false;
47 private final UploadBatcher fileBatcher;
48 private final UploadBatcher blobBatcher;
50 public AppVersionUpload(ServerConnection connection, GenericApplication app) {
51 this(connection, app, null, Boolean.TRUE);
54 /**
55 * Create a new {@link AppVersionUpload} instance that can deploy a new
56 * versions of {@code app} via {@code connection}.
58 * @param connection to connect to the server
59 * @param app that contains the code to be deployed
60 * @param backend if supplied and non-{@code null}, a particular backend is
61 * being updated
63 public AppVersionUpload(ServerConnection connection, GenericApplication app,
64 String backend, boolean batchMode) {
65 this.connection = connection;
66 this.app = app;
67 this.backend = backend;
68 fileBatcher = new UploadBatcher("file", batchMode);
69 blobBatcher = new UploadBatcher("blob", batchMode);
72 /***
73 * Uploads a new appversion to the server.
75 * @throws IOException if a problem occurs in the upload.
77 public void doUpload(ResourceLimits resourceLimits) throws IOException {
78 try {
79 File basepath = getBasepath();
81 app.statusUpdate("Scanning files on local disk.", 20);
82 int numFiles = 0;
83 long resourceTotal = 0;
84 for (File f : new FileIterator(basepath)) {
85 FileInfo fileInfo = new FileInfo(f, basepath);
86 fileInfo.setMimeType(app);
87 logger.fine("Processing file '" + f + "'.");
88 long maxFileBlobSize = fileInfo.mimeType != null ?
89 resourceLimits.maxBlobSize() : resourceLimits.maxFileSize();
90 if (f.length() > maxFileBlobSize) {
91 String message;
92 if (f.getName().toLowerCase().endsWith(".jar")) {
93 message = "Jar " + f.getPath() + " is too large. Consider "
94 + "using --enable_jar_splitting.";
95 } else {
96 message = "File " + f.getPath() + " is too large (limit "
97 + maxFileBlobSize + " bytes).";
99 throw new IOException(message);
101 resourceTotal += addFile(fileInfo);
103 if (++numFiles % 250 == 0) {
104 app.statusUpdate("Scanned " + numFiles + " files.");
107 if (numFiles > resourceLimits.maxFileCount()) {
108 throw new IOException("Applications are limited to "
109 + resourceLimits.maxFileCount() + " files, you have " + numFiles
110 + ".");
112 if (resourceTotal > resourceLimits.maxTotalFileSize()) {
113 throw new IOException("Applications are limited to "
114 + resourceLimits.maxTotalFileSize() + " bytes of resource files, "
115 + "you have " + resourceTotal + ".");
118 Collection<FileInfo> missingFiles = beginTransaction();
119 app.statusUpdate("Uploading " + missingFiles.size() + " files.", 50);
120 if (missingFiles.size() > 0) {
121 numFiles = 0;
122 int quarter = Math.max(1, missingFiles.size() / 4);
123 for (FileInfo missingFile : missingFiles) {
124 logger.fine("Uploading file '" + missingFile + "'");
125 uploadFile(missingFile);
126 if (++numFiles % quarter == 0) {
127 app.statusUpdate("Uploaded " + numFiles + " files.");
131 uploadErrorHandlers(app.getErrorHandlers(), basepath);
132 if (app.isPrecompilationEnabled()) {
133 precompile();
135 fileBatcher.flush();
136 blobBatcher.flush();
137 commit();
138 } finally {
139 rollback();
142 updateIndexes();
143 updateCron();
144 updateQueue();
145 updateDos();
146 updatePagespeed();
149 private void uploadErrorHandlers(List<ErrorHandler> errorHandlers, File basepath)
150 throws IOException {
151 if (!errorHandlers.isEmpty()) {
152 app.statusUpdate("Uploading " + errorHandlers.size() + " file(s) "
153 + "for static error handlers.");
154 for (ErrorHandler handler : errorHandlers) {
155 File file = new File(basepath, handler.getFile());
156 FileInfo info = new FileInfo(file, basepath);
157 String error = info.checkValidFilename();
158 if (error != null) {
159 throw new IOException("Could not find static error handler: " + error);
161 info.mimeType = handler.getMimeType();
162 String errorType = handler.getErrorCode();
163 if (errorType == null) {
164 errorType = "default";
166 send("/api/appversion/adderrorblob", info.file, info.mimeType, "path",
167 errorType);
172 public void precompile() throws IOException {
173 app.statusUpdate("Initializing precompilation...");
174 List<String> filesToCompile = new ArrayList<String>();
176 int errorCount = 0;
177 while (true) {
178 try {
179 filesToCompile.addAll(sendPrecompileRequest(Collections
180 .<String> emptyList()));
181 break;
182 } catch (IOException ex) {
183 if (errorCount < 3) {
184 errorCount++;
185 try {
186 Thread.sleep(1000);
187 } catch (InterruptedException ex2) {
188 IOException ex3 =
189 new IOException("Interrupted during precompilation.");
190 ex3.initCause(ex2);
191 throw ex3;
193 } else {
194 IOException ex2 =
195 new IOException(
196 "Precompilation failed. Consider adding <precompilation-enabled>false"
197 + "</precompilation-enabled> to your appengine-web.xml and trying again.");
198 ex2.initCause(ex);
199 throw ex2;
204 errorCount = 0;
205 IOException lastError = null;
206 while (!filesToCompile.isEmpty()) {
207 try {
208 if (precompileChunk(filesToCompile)) {
209 errorCount = 0;
211 } catch (IOException ex) {
212 lastError = ex;
213 errorCount++;
214 Collections.shuffle(filesToCompile);
215 try {
216 Thread.sleep(1000);
217 } catch (InterruptedException ex2) {
218 IOException ex3 =
219 new IOException("Interrupted during precompilation.");
220 ex3.initCause(ex2);
221 throw ex3;
225 if (errorCount > 3) {
226 IOException ex2 =
227 new IOException("Precompilation failed with "
228 + filesToCompile.size() + " file(s) remaining. "
229 + "Consider adding"
230 + " <precompilation-enabled>false</precompilation-enabled>"
231 + " to your " + "appengine-web.xml and trying again.");
232 ex2.initCause(lastError);
233 throw ex2;
239 * Attempt to precompile up to {@code MAX_FILES_PER_PRECOMPILE} files from
240 * {@code filesToCompile}.
242 * @param filesToCompile a list of file names, which will be mutated to remove
243 * any files that were successfully compiled.
245 * @return true if filesToCompile was reduced in size (i.e. progress was
246 * made).
248 private boolean precompileChunk(List<String> filesToCompile)
249 throws IOException {
250 int filesLeft = filesToCompile.size();
251 if (filesLeft == 0) {
252 app.statusUpdate("Initializing precompilation...");
253 } else {
254 app.statusUpdate(MessageFormat.format(
255 "Precompiling... {0} file(s) left.", filesLeft));
258 List<String> subset =
259 filesToCompile
260 .subList(0, Math.min(filesLeft, MAX_FILES_PER_PRECOMPILE));
261 List<String> remainingFiles = sendPrecompileRequest(subset);
262 subset.clear();
263 filesToCompile.addAll(remainingFiles);
264 return filesToCompile.size() < filesLeft;
267 private List<String> sendPrecompileRequest(List<String> filesToCompile)
268 throws IOException {
269 String response =
270 send("/api/appversion/precompile", Join.join("\n", filesToCompile));
271 if (response.length() > 0) {
272 return Arrays.asList(response.split("\n"));
273 } else {
274 return Collections.emptyList();
278 public void updateIndexes() throws IOException {
279 if (app.getIndexesXml() != null) {
280 app.statusUpdate("Uploading index definitions.");
281 send("/api/datastore/index/add", getIndexYaml());
286 public void updateCron() throws IOException {
287 String yaml = getCronYaml();
288 if (yaml != null) {
289 app.statusUpdate("Uploading cron jobs.");
290 send("/api/datastore/cron/update", yaml);
294 public void updateQueue() throws IOException {
295 String yaml = getQueueYaml();
296 if (yaml != null) {
297 app.statusUpdate("Uploading task queues.");
298 send("/api/queue/update", yaml);
302 public void updateDos() throws IOException {
303 String yaml = getDosYaml();
304 if (yaml != null) {
305 app.statusUpdate("Uploading DoS entries.");
306 send("/api/dos/update", yaml);
310 public void updatePagespeed() throws IOException {
311 String yaml = getPagespeedYaml();
312 if (yaml != null) {
313 app.statusUpdate("Uploading PageSpeed entries.");
314 send("/api/appversion/updatepagespeed", yaml);
315 } else {
316 try {
317 send("/api/appversion/updatepagespeed", "");
318 } catch (HttpIoException exc) {
319 if (exc.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) {
320 throw exc;
326 public void setDefaultVersion() throws IOException {
327 app.statusUpdate("Setting default version to " + app.getVersion() + ".");
328 send("/api/appversion/setdefault", "");
331 protected String getIndexYaml() {
332 return app.getIndexesXml().toYaml();
335 protected String getCronYaml() {
336 if (app.getCronXml() != null) {
337 return app.getCronXml().toYaml();
338 } else {
339 return null;
343 protected String getQueueYaml() {
344 if (app.getQueueXml() != null) {
345 return app.getQueueXml().toYaml();
346 } else {
347 return null;
351 protected String getDosYaml() {
352 if (app.getDosXml() != null) {
353 return app.getDosXml().toYaml();
354 } else {
355 return null;
359 protected String getPagespeedYaml() {
360 return app.getPagespeedYaml();
363 private File getBasepath() {
364 File path = app.getStagingDir();
365 if (path == null) {
366 path = new File(app.getPath());
368 return path;
372 * Adds a file for uploading, returning the bytes counted against the total
373 * resource quota.
375 * @param info
376 * @return 0 for a static file, or file.length() for a resource file.
377 * @throws IOException
379 private long addFile(FileInfo info) throws IOException {
380 if (inTransaction) {
381 throw new IllegalStateException("Already in a transaction.");
384 String error = info.checkValidFilename();
385 if (error != null) {
386 logger.severe(error);
387 return 0;
390 files.put(info.path, info);
392 return info.mimeType != null ? 0 : info.file.length();
396 * Begins the transaction, returning a list of files that need uploading.
398 * All calls to addFile must be made before calling beginTransaction().
400 * @return A list of pathnames that should be uploaded using uploadFile()
401 * before calling commit().
403 private Collection<FileInfo> beginTransaction() throws IOException {
404 if (inTransaction) {
405 throw new IllegalStateException("Already in a transaction.");
408 if (backend == null) {
409 app.statusUpdate("Initiating update.");
410 } else {
411 app.statusUpdate("Initiating update of backend " + backend + ".");
413 send("/api/appversion/create", app.getAppYaml());
414 inTransaction = true;
415 Collection<FileInfo> blobsToClone = new ArrayList<FileInfo>(files.size());
416 Collection<FileInfo> filesToClone = new ArrayList<FileInfo>(files.size());
418 for (FileInfo f : files.values()) {
419 if (f.mimeType == null) {
420 filesToClone.add(f);
421 } else {
422 blobsToClone.add(f);
426 TreeMap<String, FileInfo> filesToUpload = new TreeMap<String, FileInfo>();
427 cloneFiles("/api/appversion/cloneblobs", blobsToClone, "static",
428 filesToUpload);
429 cloneFiles("/api/appversion/clonefiles", filesToClone, "application",
430 filesToUpload);
432 logger.fine("Files to upload :");
433 for (FileInfo f : filesToUpload.values()) {
434 logger.fine("\t" + f);
437 this.files = filesToUpload;
438 return new ArrayList<FileInfo>(filesToUpload.values());
441 private static final int MAX_FILES_TO_CLONE = 100;
442 private static final String LIST_DELIMITER = "\n";
445 * Sends files to the given url.
447 * @param url server URL to use.
448 * @param filesParam List of files to clone.
449 * @param type Type of files ( "static" or "application")
450 * @param filesToUpload Files that need to be uploaded are added to this
451 * Collection.
453 private void cloneFiles(String url, Collection<FileInfo> filesParam,
454 String type, Map<String, FileInfo> filesToUpload) throws IOException {
455 if (filesParam.isEmpty()) {
456 return;
458 app.statusUpdate("Cloning " + filesParam.size() + " " + type + " files.");
460 int cloned = 0;
461 int remaining = filesParam.size();
462 ArrayList<FileInfo> chunk = new ArrayList<FileInfo>(MAX_FILES_TO_CLONE);
463 for (FileInfo file : filesParam) {
464 chunk.add(file);
465 if (--remaining == 0 || chunk.size() >= MAX_FILES_TO_CLONE) {
466 if (cloned > 0) {
467 app.statusUpdate("Cloned " + cloned + " files.");
469 String result = send(url, buildClonePayload(chunk));
470 if (result != null && result.length() > 0) {
471 for (String path : result.split(LIST_DELIMITER)) {
472 if (path == null || path.length() == 0) {
473 continue;
475 FileInfo info = this.files.get(path);
476 if (info == null) {
477 logger.warning("Skipping " + path + ": missing FileInfo");
478 continue;
480 filesToUpload.put(path, info);
483 cloned += chunk.size();
484 chunk.clear();
490 * Uploads a file to the hosting service.
492 * Must only be called after beginTransaction(). The file provided must be on
493 * of those that were returned by beginTransaction();
495 * @param file FileInfo for the file to upload.
497 private void uploadFile(FileInfo file) throws IOException {
498 if (!inTransaction) {
499 throw new IllegalStateException(
500 "beginTransaction() must be called before uploadFile().");
502 if (!files.containsKey(file.path)) {
503 throw new IllegalArgumentException("File " + file.path
504 + " is not in the list of files to be uploaded.");
507 files.remove(file.path);
508 if (file.mimeType == null) {
509 fileBatcher.addToBatch(file);
510 } else {
511 blobBatcher.addToBatch(file);
516 * Commits the transaction, making the new app version available.
518 * All the files returned by beginTransaction must have been uploaded with
519 * uploadFile() before commit() may be called.
521 private void commit() throws IOException {
522 deploy();
523 try {
524 boolean ready = retryWithBackoff(1, 2, 60, 20, new Callable<Boolean>() {
525 @Override
526 public Boolean call() throws Exception {
527 return isReady();
531 if (ready) {
532 startServing();
533 } else {
534 logger.severe("Version still not ready to serve, aborting.");
535 throw new RuntimeException("Version not ready.");
537 } catch (IOException ioe) {
538 throw ioe;
539 } catch (RuntimeException e) {
540 throw e;
541 } catch (Exception e) {
542 throw new RuntimeException(e);
547 * Deploys the new app version but does not make it default.
549 * All the files returned by beginTransaction must have been uploaded with
550 * uploadFile() before commit() may be called.
552 private void deploy() throws IOException {
553 if (!inTransaction) {
554 throw new IllegalStateException(
555 "beginTransaction() must be called before uploadFile().");
557 if (files.size() > 0) {
558 throw new IllegalStateException(
559 "Some required files have not been uploaded.");
561 app.statusUpdate("Deploying new version.", 20);
562 send("/api/appversion/deploy", "");
563 deployed = true;
567 * Check if the new app version is ready to serve traffic.
569 * @return true if the server returned that the app is ready to serve.
571 private boolean isReady() throws IOException {
572 if (!deployed) {
573 throw new IllegalStateException(
574 "deploy() must be called before isReady()");
576 String result = send("/api/appversion/isready", "");
577 return "1".equals(result.trim());
580 private void startServing() throws IOException {
581 if (!deployed) {
582 throw new IllegalStateException(
583 "deploy() must be called before startServing()");
585 app.statusUpdate("Closing update: new version is ready to start serving.");
586 send("/api/appversion/startserving", "");
587 inTransaction = false;
590 public void forceRollback() throws IOException {
591 app.statusUpdate("Rolling back the update" + this.backend == null ? "."
592 : " on backend " + this.backend + ".");
593 send("/api/appversion/rollback", "");
596 private void rollback() throws IOException {
597 if (!inTransaction) {
598 return;
600 forceRollback();
603 @VisibleForTesting
604 String send(String url, String payload, String... args)
605 throws IOException {
606 return connection.post(url, payload, addVersionToArgs(args));
609 @VisibleForTesting
610 String send(String url, File payload, String mimeType, String... args)
611 throws IOException {
612 return connection.post(url, payload, mimeType, addVersionToArgs(args));
615 private String[] addVersionToArgs(String... args) {
616 List<String> result = new ArrayList<String>();
617 result.addAll(Arrays.asList(args));
618 result.add("app_id");
619 result.add(app.getAppId());
620 if (backend != null) {
621 result.add("backend");
622 result.add(backend);
623 } else if (app.getVersion() != null) {
624 result.add("version");
625 result.add(app.getVersion());
627 if (app.getServer() != null) {
628 result.add("server");
629 result.add(app.getServer());
631 return result.toArray(new String[result.size()]);
635 * Calls a function multiple times, backing off more and more each time.
637 * @param initialDelay Inital delay after the first try, in seconds.
638 * @param backoffFactor Delay will be multiplied by this factor after each
639 * try.
640 * @param maxDelay Maximum delay factor.
641 * @param maxTries Maximum number of tries.
642 * @param callable Callable to call.
643 * @return true if the Callable returned true in one of its tries.
645 private boolean retryWithBackoff(double initialDelay, double backoffFactor,
646 double maxDelay, int maxTries, Callable<Boolean> callable)
647 throws Exception {
648 long delayMillis = (long) (initialDelay * 1000);
649 long maxDelayMillis = (long) (maxDelay * 1000);
650 if (callable.call()) {
651 return true;
653 while (maxTries > 1) {
654 app.statusUpdate("Will check again in " + (delayMillis / 1000)
655 + " seconds.");
656 Thread.sleep(delayMillis);
657 delayMillis *= backoffFactor;
658 if (delayMillis > maxDelayMillis) {
659 delayMillis = maxDelayMillis;
661 maxTries--;
662 if (callable.call()) {
663 return true;
666 return false;
669 private static final String TUPLE_DELIMITER = "|";
672 * Build the post body for a clone request.
674 * @param files List of FileInfos for the files to clone.
675 * @return A string containing the properly delimited tuples.
677 private static String buildClonePayload(Collection<FileInfo> files) {
678 StringBuffer data = new StringBuffer();
679 boolean first = true;
680 for (FileInfo file : files) {
681 if (first) {
682 first = false;
683 } else {
684 data.append(LIST_DELIMITER);
686 data.append(file.path);
687 data.append(TUPLE_DELIMITER);
688 data.append(file.hash);
689 if (file.mimeType != null) {
690 data.append(TUPLE_DELIMITER);
691 data.append(file.mimeType);
695 return data.toString();
698 static class FileInfo implements Comparable<FileInfo> {
699 public File file;
700 public String path;
701 public String hash;
702 public String mimeType;
704 public FileInfo(File f, File base) throws IOException {
705 this.file = f;
706 this.path = Utility.calculatePath(f, base);
707 this.hash = calculateHash();
710 public void setMimeType(GenericApplication app) {
711 mimeType = app.getMimeTypeIfStatic(path);
714 @Override
715 public String toString() {
716 return (mimeType == null ? "" : mimeType) + '\t' + hash + "\t" + path;
719 @Override
720 public int compareTo(FileInfo other) {
721 return path.compareTo(other.path);
724 @Override
725 public int hashCode() {
726 return path.hashCode();
729 @Override
730 public boolean equals(Object obj) {
731 if (obj instanceof FileInfo) {
732 return path.equals(((FileInfo) obj).path);
734 return false;
737 private static final Pattern FILE_PATH_POSITIVE_RE =
738 Pattern.compile("^[ 0-9a-zA-Z._+/$-]{1,256}$");
740 private static final Pattern FILE_PATH_NEGATIVE_RE_1 =
741 Pattern.compile("[.][.]|^[.]/|[.]$|/[.]/|^-");
743 private static final Pattern FILE_PATH_NEGATIVE_RE_2 =
744 Pattern.compile("//|/$");
746 private static final Pattern FILE_PATH_NEGATIVE_RE_3 =
747 Pattern.compile("^ | $|/ | /");
749 private String checkValidFilename() {
750 if (!FILE_PATH_POSITIVE_RE.matcher(path).matches()) {
751 return "Invalid character in filename: " + path;
753 if (FILE_PATH_NEGATIVE_RE_1.matcher(path).find()) {
754 return "Filname cannot contain '.' or '..' or start with '-': " + path;
756 if (FILE_PATH_NEGATIVE_RE_2.matcher(path).find()) {
757 return "Filname cannot have trailing / or contain //: " + path;
759 if (FILE_PATH_NEGATIVE_RE_3.matcher(path).find()) {
760 return "Any spaces must be in the middle of a filename: '" + path + "'";
762 return null;
765 private static final char[] HEX =
766 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
767 'e', 'f'};
769 public String calculateHash() throws IOException {
770 InputStream s = new FileInputStream(file);
771 byte[] buf = new byte[4096];
772 try {
773 MessageDigest digest = MessageDigest.getInstance("SHA-1");
774 for (int numRead; (numRead = s.read(buf)) != -1;) {
775 digest.update(buf, 0, numRead);
777 StringBuffer hashValue = new StringBuffer(40);
778 int i = 0;
779 for (byte b : digest.digest()) {
780 if ((i > 0) && ((i % 4) == 0)) {
781 hashValue.append('_');
783 hashValue.append(HEX[(b >> 4) & 0xf]);
784 hashValue.append(HEX[b & 0xf]);
785 ++i;
788 return hashValue.toString();
789 } catch (NoSuchAlgorithmException e) {
790 throw new RuntimeException(e);
791 } finally {
792 try {
793 s.close();
794 } catch (IOException ex) {
801 class UploadBatcher {
803 private static final int MAX_BATCH_SIZE = 3200000;
804 private static final int MAX_BATCH_COUNT = 100;
805 private static final int MAX_BATCH_FILE_SIZE = 200000;
806 private static final int BATCH_OVERHEAD = 500;
808 String what;
809 String singleUrl;
810 String batchUrl;
811 boolean batching = true;
812 List<FileInfo> batch = new ArrayList<FileInfo>();
813 long batchSize = 0;
816 * @param what Either "file" or "blob" or "errorblob" indicating what kind of objects this
817 * batcher uploads. Used in messages and URLs.
818 * @param batching whether or not we want to really do batch.
820 public UploadBatcher(String what, boolean batching) {
821 this.what = what;
822 this.singleUrl = "/api/appversion/add" + what;
823 this.batchUrl = singleUrl + "s";
824 this.batching = batching;
828 * Send the current batch on its way and reset the batrch buffer when done
830 public void sendBatch() throws IOException {
832 app.statusUpdate(
833 "Sending batch containing " + batch.size() + " "+ what +"(s) totaling " +
834 batchSize / 1000 + "KB.");
835 connection.post(batchUrl, batch, addVersionToArgs("", ""));
836 batch = new ArrayList<FileInfo>();
837 batchSize = 0;
841 * """Flush the current batch.
843 * This first attempts to send the batch as a single request; if that fails because the server
844 * doesn"t support batching, the files are sent one by one, and self.batching is reset to
845 * False.
847 * At the end, self.batch and self.batchSize are reset
849 public void flush() throws IOException {
850 if (batch.isEmpty()) {
851 return;
853 try {
854 sendBatch();
855 } catch (Exception e) {
856 app.statusUpdate("Exception in flushing batch payload, so sending 1 by 1..."
857 + e.getMessage());
858 batching = false;
859 for (FileInfo fileInfo : batch) {
860 send(singleUrl, fileInfo.file, fileInfo.mimeType, "path", fileInfo.path);
862 batch = new ArrayList<FileInfo>();
863 batchSize = 0;
868 * Batch a file, possibly flushing first, or perhaps upload it directly.
870 * Args: path: The name of the file. payload: The contents of the file. mime_type: The MIME
871 * Content-type of the file, or None.
873 * If mime_type is None, application/octet-stream is substituted. """
875 public void addToBatch(FileInfo fileInfo) throws IOException {
877 long size = fileInfo.file.length();
879 if (size <= MAX_BATCH_FILE_SIZE) {
880 if ((batch.size() >= MAX_BATCH_COUNT) ||
881 (batchSize + size > MAX_BATCH_SIZE)) {
882 flush();
884 if (batching) {
885 batch.add(fileInfo);
886 batchSize += size + BATCH_OVERHEAD;
887 return;
890 send(singleUrl, fileInfo.file, fileInfo.mimeType, "path", fileInfo.path);