All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.slacesa.simpleSwiftClient.SimpleSwiftClient Maven / Gradle / Ivy

package io.github.slacesa.simpleSwiftClient;

import java.io.FileNotFoundException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

import org.joda.time.DateTime;

import io.github.slacesa.simpleSwiftClient.resources.AuthMessage;
import io.github.slacesa.simpleSwiftClient.resources.SwiftConfig;
import io.github.slacesa.simpleSwiftClient.resources.SwiftFile;
import io.github.slacesa.zipper.Zipper;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.impl.NoStackTraceThrowable;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;

/**
 * Simple Swift Client, used to connect to authenticate, upload, list and download files from cold storage
 * typical usage for upload is: retrieveClient() to get a valid client once and then uploadFile() every time you need it.
 * @author SLC
 * @version 1.0
 */
public class SimpleSwiftClient {

	private SwiftConfig config;
	private String token;
	private DateTime token_expires;
	private Vertx vertx;
	private WebClient webclient;
	private Zipper zipper;

	private static SimpleSwiftClient swiftClient;
	
	private static final Logger log = Logger.getLogger(SimpleSwiftClient.class.getName());

	/**
	 * Retrieve a singleton simple Swift Client, if already initialized it overwrites previous configuration
	 * @param config the required config
	 * @return a future to a simple swift client
	 */
	public static Future retrieveClient(SwiftConfig config) {
		Promise result = Promise.promise();
		boolean force = (swiftClient != null);
		if(force) swiftClient.config = config;
		else swiftClient = new SimpleSwiftClient(config);

		swiftClient.retrieveToken(force).onComplete(ar -> {
			if(ar.succeeded())
				result.complete(swiftClient);
			else
				result.fail(ar.cause());
		});
		return result.future();
	}

	/**
	 * Retrieve the singleton simple Swift Client, if already initialized, fails otherwise
	 * @return a future to a simple swift client
	 */
	public static Future retrieveClient() {
		Promise result = Promise.promise();		
		if(swiftClient != null) result.complete(swiftClient);
		else result.fail(new NullPointerException("The client has not been initialized"));
		return result.future();
	}

	/**
	 * Retrieves a list of current files, including information on availability (policy_retrieval_state [sealed, unsealing, unsealed] and policy_retrieval_delay)
	 * @return an array containing a list of files
	 */
	public Future> getFileList() {
		Promise> result = Promise.promise();
		getter(null).onComplete(response -> {
			try {
				if(response.succeeded() && response.result().statusCode() == 200)
					result.complete(Arrays.asList(Json.decodeValue(response.result().bodyAsBuffer(), SwiftFile[].class)));
				else if(response.succeeded() && response.result().statusCode() == 404)
					result.fail(new FileNotFoundException("Container not found"));
			}
			catch (Exception e) {
				result.fail(e.getCause());
			}
		});
		return result.future();
	}

	/**
	 * Unseals a file, retrieves the time when it will be ready
	 * @param filename the file name to unseal
	 * @return a future time (in seconds) when the file will be ready
	 */
	public Future unsealFile(String filename) {
		Promise result = Promise.promise();
		getter(filename).onComplete(response -> {
			if(response.succeeded()) {
				if(response.result().statusCode() == 429)
					result.complete(Integer.valueOf(response.result().getHeader("Retry-After")));
				else if(response.result().statusCode() == 200)
					result.complete(0);
				else if(response.result().statusCode() == 404)
					result.fail(new FileNotFoundException("File not found: " + filename));
				else result.fail(new Exception("Unknown status code: " + response.result().statusCode()));
			}
			else result.fail(response.cause());
		});
		return result.future();
	}

	/**
	 * Retrieves a buffer to a file (if the file is unsealed)
	 * @param filename the file name to retrieve
	 * @return a future buffer to the file
	 */
	public Future downloadFile(String filename) {
		Promise result = Promise.promise();
		getter(filename).onComplete(response -> {
			if(response.succeeded()) {
				if(response.result().statusCode() == 200)
					result.complete(response.result().bodyAsBuffer());
				else if(response.result().statusCode() == 429)
					result.fail("Not ready, try again in " + response.result().getHeader("Retry-After"));
				else if(response.result().statusCode() == 404)
					result.fail(new FileNotFoundException("File not found: " + filename));
				else result.fail(new Exception("Unknown status code: " + response.result().statusCode()));
			}
			else result.fail(response.cause());
		});
		return result.future();
	}

	/**
	 * Reads a file from disk and uploads it to default folder
	 * TODO Maybe allow to choose target folder
	 * @param filename the source file name
	 * @return a future boolean to result
	 */
	public Future uploadFile(String filename) {
		Promise result = Promise.promise();
		localReadFile(filename).compose(fileContent -> 
		putter(filename, fileContent).onComplete(isSent ->{
			result.complete(isSent.succeeded() && isSent.result());
		})).otherwise(t1 -> {
			result.fail(new NoStackTraceThrowable("File not found"));
			return null;
		});
		return result.future();
	}

	/**
	 * Uploads a file with target name and target content and stores it to default folder
	 * TODO Maybe allow to choose target folder
	 * @param filename the source file name
	 * @param fileContent the data the new file will contain
	 * @return a future boolean to result
	 */
	public Future uploadFile(String filename, Buffer fileContent) {
		Promise result = Promise.promise(); 
		putter(filename, fileContent).onComplete(isSent ->{
			result.complete(isSent.succeeded() && isSent.result());
		});
		return result.future();
	}

	/**
	 * Deletes a file
	 * @param filename the file name to delete
	 * @return a future true if the file was deleted
	 */
	public Future deleteFile(String filename) {
		Promise result = Promise.promise();
		deleter(filename).onComplete(isDeleted ->{
			result.complete(isDeleted.succeeded() && isDeleted.result());
		});
		return result.future();
	}

	/**
	 * Used to backup a folder on our remote swift container using a password protected zip file
	 * @param folderPath the source file, the last part will be used to generate the zip file name (e.g. ancestor/parent/child becomes child.zip)
	 * @param password the password used to protect the zip file
	 * @return a void promise, successful if the folder was zipped, sent and the temp zip was deleted 
	 */
	public Future backupFolder(String folderPath, String password) {
		Promise result = Promise.promise();
		String[] folders = folderPath.split("/");
		String zipFileName = folders[folders.length-1]+".zip";
		zipper.zipFolder(zipFileName, folderPath, password).compose(v ->
		uploadFile(zipFileName).compose(isSent ->
		localDeleteFile(zipFileName).onComplete(v2 -> {
			if(v2.succeeded()) result.complete();
		}))).otherwise(err -> {
			result.fail(err.getCause());
			return null;
		});
		return result.future();
	}

	/**
	 * Closes the client and frees all resources
	 */
	public void close() {
		webclient.close();
	}

	/**
	 * A minimal Swift Client
	 * @param username a Keystone user, with storage rights
	 * @param password a Keystone password
	 */
	private SimpleSwiftClient(SwiftConfig config) {
		this.config = config;
		this.vertx = Vertx.currentContext().owner();
		this.webclient = WebClient.create(vertx);
		this.zipper = Zipper.getZipper(vertx);
	}

	/**
	 * Retrieve a valid token
	 * Checks if there is a current valid token, otherwise it retrieves one
	 * @param force ignores the validity check, retrieves a new token
	 * @return a future to a valid token
	 */
	private Future retrieveToken(boolean force) {
		Promise result = Promise.promise();
		if(!force && token != null && token_expires != null && token_expires.isAfterNow()) {
			result.complete(token);
		}
		else {
			JsonObject authMessage = new AuthMessage(config.getUsername(), config.getPassword()).parse();
			webclient.post(
					config.getPort(),
					config.getAuth_host(),
					config.getAuth_endpoint())
			.ssl(config.getPort()==443)
			.putHeader("Content-Type", "application/json")
			.sendJsonObject(authMessage, ar -> {
				if(ar.succeeded() && (ar.result().statusCode() == 200 || ar.result().statusCode() == 201)) {
					token = ar.result().getHeader("X-Subject-Token");
					log.fine("Token " + token);
					JsonObject bodyResponse = ar.result().bodyAsJsonObject();
					token_expires = DateTime.parse(bodyResponse.getJsonObject("token").getString("expires_at"));
					// Sets a timer to refresh token one hour before expiration
					vertx.setTimer(token_expires.minusHours(1).getMillis()-DateTime.now().getMillis(), timer -> {
						retrieveToken(true);
					});
					result.complete(token);
				}
				else {
					//Fail, but retry after 1 minute
					token = null;
					vertx.setTimer(60000, timer -> {
						retrieveToken(true);
					});
					//log.info(String.valueOf(ar.result().statusCode()));
					//log.info(ar.result().bodyAsString());
					result.fail(ar.succeeded()?new NoStackTraceThrowable("Status Code not 200 OK"):ar.cause());
				}
			});
		}
		return result.future();
	}

	/**
	 * Multi purpose getter: can request list of files (filename == null), unseal a file (status code 429) or retrieve a file (status code 200), depending on circustances
	 * @return an HttpResponse with the result
	 */
	private Future> getter(String filename) {
		Promise> result = Promise.promise();
		String extra = (filename == null)? "?policy_extra=true" : "/" + filename;
		webclient.get(
				config.getPort(),
				config.getStorage_host(),
				config.getStorage_endpoint()+extra)
		.ssl(config.getPort()==443)
		.putHeader("Accept", "application/json")
		.putHeader("X-Auth-Token", token)
		.send(response -> {
			if(response.succeeded())
				result.complete(response.result());
			else result.fail(response.cause());
		});
		return result.future();
	}

	/**
	 * Multi purpose deleter: can delete a container if empty (filename == null) or delete a file (returns 204 when successful)
	 * @return an HttpResponse with the result
	 */
	private Future deleter(String filename) {
		Promise result = Promise.promise();
		webclient.delete(
				config.getPort(),
				config.getStorage_host(),
				config.getStorage_endpoint()+"/"+filename)
		.ssl(config.getPort()==443)
		.putHeader("Accept", "application/json")
		.putHeader("X-Auth-Token", token)
		.send(response -> {
			if(response.succeeded())
				result.complete(response.result().statusCode() == 204);
			else result.fail(response.cause());
		});
		return result.future();
	}

	/**
	 * Reads target file
	 * @param filename the file name
	 * @return a future data buffer to target file
	 */
	private Future localReadFile(String filename) {
		Promise result = Promise.promise();
		vertx.fileSystem().readFile(filename, ar -> {
			if(ar.succeeded()) result.complete(ar.result());
			else result.fail(ar.cause());
//			log.debug("File length " + ar.result().length());
		});
		return result.future();
	}

	/**
	 * Deletes target file
	 * @param filename the file name
	 * @return a void future
	 */
	private Future localDeleteFile(String filename) {
		Promise result = Promise.promise();
		vertx.fileSystem().delete(filename, ar -> {
			if(ar.succeeded()) result.complete();
			else result.fail(ar.cause());
		});
		return result.future();
	}

	/**
	 * Puts a file names filename to default folder containing given fileContent
	 * TODO Maybe allow to choose target folder
	 * @param filename the file name
	 * @param fileContent the data buffer to write
	 * @return a future boolean to result
	 */
	private Future putter(String filename, Buffer fileContent) {
		Promise result = Promise.promise();
		webclient.put(
				config.getPort(),
				config.getStorage_host(),
				config.getStorage_endpoint()+"/"+filename)
		.ssl(config.getPort()==443)
		.putHeader("X-Storage-Policy", "PCA")
		.putHeader("X-Auth-Token", token)
		.putHeader("Content-Length", Integer.toString(fileContent.length()))
		.putHeader("Etag", computeMD5(fileContent))
		.sendBuffer(fileContent, ar -> {
			result.complete(ar.succeeded() && ar.result().statusCode() == 201);
			if(!ar.succeeded()) result.fail(ar.cause());
		});
		return result.future();
	}

	private final static char[] hexArray = "0123456789abcdef".toCharArray();

	private static String bytesToHex(byte[] bytes) {
		char[] hexChars = new char[bytes.length * 2];
		for ( int j = 0; j < bytes.length; j++ ) {
			int v = bytes[j] & 0xFF;
			hexChars[j * 2] = hexArray[v >>> 4];
			hexChars[j * 2 + 1] = hexArray[v & 0x0F];
		}
		return new String(hexChars);
	}

	private String computeMD5(Buffer fileContent) {
		String result = null;
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			result = bytesToHex(md.digest(fileContent.getBytes()));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return result;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy