diff --git a/.travis.yml b/.travis.yml index a2e3a69..8644191 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,5 +11,8 @@ cache: jdk: - oraclejdk8 +script: + - TEST_TYPE='unit' ./gradlew check + after_success: - ./gradlew jacocoTestReport coveralls diff --git a/build.gradle b/build.gradle index b057508..ee44a5e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,16 +11,20 @@ apply plugin: 'maven-publish' group = 'com.filestack' sourceCompatibility = 1.7 -version = getVersion() +version = file(new File('VERSION')).text.trim() // Get version string from VERSION text file -repositories { - jcenter() +// ***************************************** Config *********************************************** + +configurations { + integTestCompile.extendsFrom testCompile + integTestRuntime.extendsFrom testRuntime + integTestImplementation.extendsFrom testImplementation } dependencies { testImplementation 'junit:junit:4.12' // Testing testImplementation 'org.mockito:mockito-core:2.8.47' // Mocking - testImplementation 'com.squareup.retrofit2:retrofit-mock:2.3.0' // Testing helpers for Retrofit + testImplementation 'com.squareup.retrofit2:retrofit-mock:2.3.0' // Helpers for Retrofit compile 'com.squareup.okhttp3:okhttp:3.8.0' // Low-level HTTP client compile 'com.squareup.retrofit2:retrofit:2.3.0' // High-level HTTP client @@ -34,36 +38,40 @@ dependencies { compile 'io.reactivex.rxjava2:rxjava:2.1.2' // Observable pattern for async methods } - -// ***************************************** General Config *************************************** - -checkstyle { - toolVersion '8.1' -} - -jacocoTestReport { - reports { - xml.enabled = true - html.enabled = true - } -} - javadoc { destinationDir new File("./docs") options.optionFiles(new File('./config/javadoc/javadoc.txt')) } -// ***************************************** General Config *************************************** - +// Publications define artifacts to upload to Bintray +publishing { + publications { + Maven(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + } + } +} -// ***************************************** Version Config *************************************** +repositories { + jcenter() +} -// Get version string from VERSION text file -def getVersion() { - return file(new File('VERSION')).text.trim() +sourceSets { + integTest { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integTest/java') + } + resources.srcDir file('src/integTest/resources') + } } -// Create version properties file from VERSION text file +// ***************************************** Tasks ************************************************ + +// Output version to version.properties file task createProperties(dependsOn: processResources) { doLast { new File("$buildDir/resources/main/version.properties").withWriter { w -> @@ -74,28 +82,33 @@ task createProperties(dependsOn: processResources) { } } -// Add task to build process -classes { - dependsOn createProperties +// Run integration tests +task integTest(type: Test) { + testClassesDir = sourceSets.integTest.output.classesDir + classpath = sourceSets.integTest.runtimeClasspath + outputs.upToDateWhen { false } } -// ***************************************** Version Config *************************************** - +// Create javadoc artifact jar +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} -// ***************************************** Bintray Config *************************************** +// Create source artifact jar +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} -// Publications define artifacts to upload to Bintray -publishing { - publications { - Maven(MavenPublication) { - from components.java - artifact sourcesJar - artifact javadocJar - } - } +classes.dependsOn createProperties // Create version.properties as part of build +integTest.mustRunAfter test // Run integration tests after unit tests +tasks.withType(Test) { // Put unit and integration test reports in separate directories + reports.html.destination = file("${reporting.baseDir}/${name}") } -// Bintray publishing config +// *************************************** Plugin Config ****************************************** + bintray { user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') @@ -127,15 +140,13 @@ bintray { } } -// Tasks for creating artifact jars -task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource +checkstyle { + toolVersion '8.1' } -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir +jacocoTestReport { + reports { + xml.enabled = true + html.enabled = true + } } - -// ***************************************** Bintray Config *************************************** diff --git a/src/main/java/com/filestack/FileLink.java b/src/main/java/com/filestack/FileLink.java index 668cd74..936e918 100644 --- a/src/main/java/com/filestack/FileLink.java +++ b/src/main/java/com/filestack/FileLink.java @@ -6,8 +6,10 @@ import com.filestack.errors.ResourceNotFoundException; import com.filestack.errors.ValidationException; import com.filestack.responses.ImageTagResponse; +import com.filestack.transforms.AvTransform; import com.filestack.transforms.ImageTransform; import com.filestack.transforms.ImageTransformTask; +import com.filestack.transforms.tasks.AvTransformOptions; import com.filestack.util.FsService; import com.filestack.util.Util; import com.google.gson.Gson; @@ -307,6 +309,28 @@ public boolean imageSfw() return json.get("sfw").getAsBoolean(); } + /** + * Creates an {@link AvTransform} object for this file using default storage options. + * + * @see #avTransform(StorageOptions, AvTransformOptions) + */ + public AvTransform avTransform(AvTransformOptions avOptions) { + return avTransform(null, avOptions); + } + + /** + * Creates an {@link AvTransform} object for this file using custom storage options. + * A transformation call isn't made directly by this method. + * For both audio and video transformations. + * + * @param storeOptions options for how to save the file(s) in your storage backend + * @param avOptions options for how ot convert the file + * @return {@link AvTransform ImageTransform} instance configured for this file + */ + public AvTransform avTransform(StorageOptions storeOptions, AvTransformOptions avOptions) { + return new AvTransform(this, storeOptions, avOptions); + } + // Async method wrappers /** diff --git a/src/main/java/com/filestack/FilestackClient.java b/src/main/java/com/filestack/FilestackClient.java index d3d9a2a..12a38df 100644 --- a/src/main/java/com/filestack/FilestackClient.java +++ b/src/main/java/com/filestack/FilestackClient.java @@ -88,7 +88,7 @@ public FilestackClient build() { /** * Upload local file using default storage options. * - * @see #upload(String, UploadOptions) + * @see #upload(String, StorageOptions, boolean) */ public FileLink upload(String pathname) throws ValidationException, IOException, PolicySignatureException, @@ -99,8 +99,20 @@ public FileLink upload(String pathname) /** * Upload local file using custom storage options. * - * @param pathname path to the file, can be local or absolute - * @param options storage options, https://www.filestack.com/docs/rest-api/store + * @see #upload(String, StorageOptions, boolean) + */ + public FileLink upload(String pathname, StorageOptions options) + throws ValidationException, IOException, PolicySignatureException, + InvalidParameterException, InternalException { + return upload(pathname, options, true); + } + + /** + * Upload local file using custom storage and upload options. + * + * @param pathname path to the file, can be local or absolute + * @param options storage options, https://www.filestack.com/docs/rest-api/store + * @param intelligent intelligent ingestion, improves reliability for bad networks * @return new {@link FileLink} referencing file * @throws ValidationException if the pathname doesn't exist or isn't a regular file * @throws IOException if request fails because of network or other IO issue @@ -108,14 +120,15 @@ public FileLink upload(String pathname) * @throws InvalidParameterException if a request parameter is missing or invalid * @throws InternalException if unexpected error occurs */ - public FileLink upload(String pathname, UploadOptions options) + public FileLink upload(String pathname, StorageOptions options, boolean intelligent) throws ValidationException, IOException, PolicySignatureException, InvalidParameterException, InternalException { + if (options == null) { - options = new UploadOptions.Builder().build(); + options = new StorageOptions.Builder().build(); } - Upload upload = new Upload(pathname, this, options, fsService, delayBase); + Upload upload = new Upload(pathname, options, intelligent, delayBase, this, fsService); return upload.run(); } @@ -124,7 +137,7 @@ public FileLink upload(String pathname, UploadOptions options) /** * Asynchronously upload local file using default storage options. * - * @see #upload(String, UploadOptions) + * @see #upload(String, StorageOptions, boolean) */ public Single uploadAsync(String pathname) { return uploadAsync(pathname, null); @@ -133,13 +146,24 @@ public Single uploadAsync(String pathname) { /** * Asynchronously upload local file using custom storage options. * - * @see #upload(String, UploadOptions) + * @see #upload(String, StorageOptions, boolean) */ - public Single uploadAsync(final String pathname, final UploadOptions options) { + public Single uploadAsync(final String pathname, final StorageOptions options) { + return uploadAsync(pathname, options, true); + } + + /** + * Asynchronously upload local file using custom storage and upload options. + * + * @see #upload(String, StorageOptions, boolean) + */ + public Single uploadAsync(final String pathname, final StorageOptions options, + final boolean intelligent) { + return Single.fromCallable(new Callable() { @Override public FileLink call() throws Exception { - return upload(pathname, options); + return upload(pathname, options, intelligent); } }) .subscribeOn(Schedulers.io()) diff --git a/src/main/java/com/filestack/StorageOptions.java b/src/main/java/com/filestack/StorageOptions.java new file mode 100644 index 0000000..9504475 --- /dev/null +++ b/src/main/java/com/filestack/StorageOptions.java @@ -0,0 +1,115 @@ +package com.filestack; + +import com.filestack.transforms.TransformTask; +import com.filestack.util.Util; +import java.util.HashMap; +import java.util.Map; +import okhttp3.RequestBody; + +/** Configure storage options for uploads and transformation stores. */ +public class StorageOptions { + private String access; + private Boolean base64Decode; + private String container; + private String filename; + private String location; + private String path; + private String region; + + public StorageOptions() { } + + /** Get these options as a task. */ + public TransformTask getAsTask() { + TransformTask task = new TransformTask("store"); + addToTask(task); + return task; + } + + /** Add these options to an existing task. */ + public void addToTask(TransformTask task) { + task.addOption("access", access); + task.addOption("base64decode", base64Decode); + task.addOption("container", container); + task.addOption("filename", filename); + task.addOption("location", location); + task.addOption("path", path); + task.addOption("region", region); + } + + /** Get these options as a part map to use for uploads. */ + public Map getAsPartMap() { + HashMap map = new HashMap<>(); + addToMap(map, "store_access", access); + addToMap(map, "store_container", container); + addToMap(map, "store_location", location != null ? location : "s3"); + addToMap(map, "store_path", path); + addToMap(map, "store_region", region); + return map; + } + + private static void addToMap(Map map, String key, String value) { + if (value != null) { + map.put(key, Util.createStringPart(value)); + } + } + + public static class Builder { + private String access; + private Boolean base64Decode; + private String container; + private String filename; + private String location; + private String path; + private String region; + + public Builder access(String access) { + this.access = access; + return this; + } + + public Builder base64Decode(boolean base64Decode) { + this.base64Decode = base64Decode; + return this; + } + + public Builder container(String container) { + this.container = container; + return this; + } + + public Builder filename(String filename) { + this.filename = filename; + return this; + } + + public Builder location(String location) { + this.location = location; + return this; + } + + public Builder path(String path) { + this.path = path; + return this; + } + + public Builder region(String region) { + this.region = region; + return this; + } + + /** + * Builds new {@link StorageOptions}. + */ + public StorageOptions build() { + StorageOptions building = new StorageOptions(); + building.access = access; + building.base64Decode = base64Decode; + building.container = container; + building.filename = filename; + building.location = location; + building.path = path; + building.region = region; + return building; + } + } +} diff --git a/src/main/java/com/filestack/UploadOptions.java b/src/main/java/com/filestack/UploadOptions.java deleted file mode 100644 index e6c4d32..0000000 --- a/src/main/java/com/filestack/UploadOptions.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.filestack; - -import com.filestack.util.Util; - -import java.util.HashMap; -import java.util.Map; - -import okhttp3.RequestBody; - -/** - * Configures options for an upload. - */ -public class UploadOptions { - HashMap map; - - UploadOptions(HashMap map) { - this.map = map; - } - - public Map getMap() { - return map; - } - - // Javadoc comments adapted from - // https://github.com/filepicker/filestack-uploads/blob/develop/README.md - - /** - * Builds new {@link UploadOptions}. - */ - public static class Builder { - HashMap map = new HashMap<>(); - - /** - * Set the location where the file will be stored. For locations other than s3 the file will be - * first stored in Filestack's internal S3 bucket and then moved to a proper location. - * If empty or not valid the file will be stored in S3 bucket configured for the selected - * application (if configured) or in Filestack's internal S3 bucket. - * - * @param location s3, gcs, dropbox, azure, or rackspace - */ - public Builder location(String location) { - map.put("store_location", Util.createStringPart(location)); - return this; - } - - /** - * Set an S3 region for the selected S3 bucket. If {@link #container(String)} is provided and - * this is empty, the application will try to get the region from database or directly from - * Amazon. - */ - public Builder region(String region) { - map.put("store_region", Util.createStringPart(region)); - return this; - } - - /** Set the name of the container where the file will be stored. */ - public Builder container(String container) { - map.put("store_container", Util.createStringPart(container)); - return this; - } - - /** - * Set the path where the file will be stored. By default, the file is stored at the root of the - * container at a unique id, followed by an underscore, followed by the filename. Paths ending - * with / will be treated as folders where the files will be stored in a similar way. Ignored - * if the file is stored in Filestack's internal S3 bucket. - */ - public Builder path(String path) { - map.put("store_path", Util.createStringPart(path)); - return this; - } - - /** - * Set if the file should be stored in a way that allows public access going directly to - * the underlying file store. If empty or the file is stored in Filestack's internal S3 bucket - * it defaults to private. - * - * @param access private or public - */ - public Builder access(String access) { - map.put("store_access", Util.createStringPart(access)); - return this; - } - - /** - * Sets if the upload should use Filestack Intelligent Ingestion. Enabled by default. - * This must also be enabled on your account to work, if not it will be ignored. - */ - public Builder intelligent(boolean intelligent) { - map.put("multipart", Util.createStringPart(Boolean.toString(intelligent))); - return this; - } - - /** - * Create the {@link UploadOptions} instance using the configured values. - */ - public UploadOptions build() { - if (!map.containsKey("store_location")) { - map.put("store_location", Util.createStringPart("s3")); - } - if (!map.containsKey("multipart")) { - map.put("multipart", Util.createStringPart("true")); - } - return new UploadOptions(map); - } - } -} diff --git a/src/main/java/com/filestack/transforms/AvTransform.java b/src/main/java/com/filestack/transforms/AvTransform.java new file mode 100644 index 0000000..fbe5430 --- /dev/null +++ b/src/main/java/com/filestack/transforms/AvTransform.java @@ -0,0 +1,113 @@ +package com.filestack.transforms; + +import com.filestack.FileLink; +import com.filestack.StorageOptions; +import com.filestack.errors.InternalException; +import com.filestack.errors.InvalidArgumentException; +import com.filestack.errors.InvalidParameterException; +import com.filestack.errors.PolicySignatureException; +import com.filestack.errors.ResourceNotFoundException; +import com.filestack.transforms.tasks.AvTransformOptions; +import com.filestack.util.Util; +import com.google.gson.JsonObject; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; +import java.io.IOException; +import java.util.concurrent.Callable; + +/** + * {@link Transform Transform} subclass for audio and video transformations. + */ +public class AvTransform extends Transform { + + /** + * Constructs a new instance. + * + * @param fileLink must point to an existing audio or video resource + * @param storeOpts sets how the resulting file(s) are stored, uses defaults if null + * @param avOps sets conversion options + */ + public AvTransform(FileLink fileLink, StorageOptions storeOpts, AvTransformOptions avOps) { + + super(fileLink); + + if (avOps == null) { + throw new InvalidArgumentException("AvTransform can't be created without options"); + } + + if (storeOpts != null) { + tasks.add(TransformTask.merge("video_convert", storeOpts.getAsTask(), avOps)); + } else { + tasks.add(avOps); + } + } + + /** + * Gets converted content as a new {@link FileLink}. Starts processing on first call. + * Returns null if still processing. Poll this method or use {@link #getFileLinkAsync()}. + * If you need other data, such as thumbnails, use {@link Transform#getContentJson()}. + * + * @return null if processing, new {@link FileLink} if complete + * @throws IOException if request fails because of network or other IO issue + * @throws PolicySignatureException if security is missing or invalid or tagging isn't enabled + * @throws ResourceNotFoundException if handle isn't found + * @throws InvalidParameterException if handle is malformed + * @throws InternalException if unexpected error occurs + */ + public FileLink getFileLink() + throws IOException, PolicySignatureException, ResourceNotFoundException, + InvalidParameterException, InternalException { + + JsonObject json = getContentJson(); + String status = json.get("status").getAsString(); + + switch (status) { + case "started": + case "pending": + return null; + case "completed": + JsonObject data = json.get("data").getAsJsonObject(); + String url = data.get("url").getAsString(); + String handle = url.split("/")[3]; + return new FileLink(apiKey, handle, security); + default: + throw new InternalException(); + } + } + + // Async method wrappers + + /** + * Asynchronously gets converted content as a new {@link FileLink}. + * Uses default 10 second polling. Use {@link #getFileLinkAsync(int)} to adjust interval. + * + * @see #getFileLink() + */ + public Single getFileLinkAsync() { + return getFileLinkAsync(10); + } + + /** + * Asynchronously gets converted content as a new {@link FileLink}. + * + * @param pollInterval how frequently to poll (in seconds) + * @see #getFileLink() + */ + public Single getFileLinkAsync(final int pollInterval) { + return Single.fromCallable(new Callable() { + @Override + public FileLink call() throws Exception { + FileLink fileLink = null; + while (fileLink == null) { + fileLink = getFileLink(); + if (!Util.isUnitTest()) { + Thread.sleep(pollInterval * 1000); + } + } + return fileLink; + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.single()); + } +} diff --git a/src/main/java/com/filestack/transforms/ImageTransform.java b/src/main/java/com/filestack/transforms/ImageTransform.java index aff905d..d5d30d0 100644 --- a/src/main/java/com/filestack/transforms/ImageTransform.java +++ b/src/main/java/com/filestack/transforms/ImageTransform.java @@ -2,12 +2,12 @@ import com.filestack.FileLink; import com.filestack.FilestackClient; +import com.filestack.StorageOptions; import com.filestack.errors.InternalException; import com.filestack.errors.InvalidParameterException; import com.filestack.errors.PolicySignatureException; import com.filestack.errors.ResourceNotFoundException; import com.filestack.responses.StoreResponse; -import com.filestack.transforms.tasks.StoreOptions; import com.filestack.util.Util; import com.google.gson.JsonObject; import io.reactivex.Single; @@ -85,7 +85,7 @@ public FileLink store() * Stores the result of a transformation into a new file. * @see * - * @param storeOptions configure where and how your file is stored + * @param storageOptions configure where and how your file is stored * @return new {@link FileLink FileLink} pointing to the file * @throws IOException if request fails because of network or other IO issue * @throws PolicySignatureException if security is missing or invalid @@ -93,14 +93,15 @@ public FileLink store() * @throws InvalidParameterException if a request parameter is missing or invalid * @throws InternalException if unexpected error occurs */ - public FileLink store(StoreOptions storeOptions) + public FileLink store(StorageOptions storageOptions) throws IOException, PolicySignatureException, ResourceNotFoundException, InvalidParameterException, InternalException { - if (storeOptions == null) { - storeOptions = new StoreOptions(); + if (storageOptions == null) { + storageOptions = new StorageOptions(); } - addTask(storeOptions); + + tasks.add(storageOptions.getAsTask()); Response response; String tasksString = getTasksString(); @@ -160,14 +161,14 @@ public Single storeAsync() { } /** - * Async, observable version of {@link #store(StoreOptions)}. + * Async, observable version of {@link #store(StorageOptions)}. * Same exceptions are passed through observable. */ - public Single storeAsync(final StoreOptions storeOptions) { + public Single storeAsync(final StorageOptions storageOptions) { return Single.fromCallable(new Callable() { @Override public FileLink call() throws Exception { - return store(storeOptions); + return store(storageOptions); } }) .subscribeOn(Schedulers.io()) diff --git a/src/main/java/com/filestack/transforms/TransformTask.java b/src/main/java/com/filestack/transforms/TransformTask.java index 0f01adf..3033d5b 100644 --- a/src/main/java/com/filestack/transforms/TransformTask.java +++ b/src/main/java/com/filestack/transforms/TransformTask.java @@ -3,6 +3,7 @@ import com.filestack.errors.InvalidArgumentException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; /** * Generic transform task object. @@ -12,7 +13,7 @@ public class TransformTask { String name; ArrayList