diff --git a/.gitignore b/.gitignore index ac68908..bac0399 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.settings/ **/*.csv *.properties +*.db diff --git a/pom.xml b/pom.xml index 6455b4d..c56d4de 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,14 @@ 1.8 1.8 6.0.2 + 2.11.3 + 7.3.0 + 1.4.200 + 5.4.24.Final + 5.3.1 + 2.4.0 + 2.4.1 + 3.0.0-M2 @@ -20,7 +28,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M2 + ${maven.enforcer.version} @@ -53,6 +61,14 @@ + + org.codehaus.mojo + versions-maven-plugin + 2.7 + + false + + org.apache.maven.plugins maven-shade-plugin @@ -68,7 +84,7 @@ - net.locusworks.s3sync.Entry + net.locusworks.s3sync.S3SyncLauncher @@ -85,6 +101,39 @@ + + org.flywaydb + flyway-maven-plugin + ${flyway.version} + + flyway-h2 + sa + jdbc:h2:file:./s3sync;MODE=MYSQL;DATABASE_TO_UPPER=false + false + + s3sync + + _flyway_migration
+ + filesystem:${basedir}/src/main/resources/database/migration + +
+ + + process-sources + + migrate + + + + + + com.h2database + h2 + ${h2.version} + + +
@@ -95,6 +144,27 @@ 1.11.892 + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + org.apache.commons @@ -127,6 +197,127 @@ 0.1.55 + + org.hibernate + hibernate-core + ${hibernate.version} + + + org.jboss.logging + jboss-logging + + + + + + org.hibernate + hibernate-entitymanager + ${hibernate.version} + + + + org.hibernate + hibernate-hikaricp + ${hibernate.version} + + + org.slf4j + slf4j-api + + + + + + + org.springframework.boot + spring-boot + ${spring.boot.version} + + + org.springframework + spring-context + + + org.springframework + spring-core + + + org.hibernate + hibernate-core + + + org.apache.logging.log4j + log4j-core + + + + + + + org.springframework.boot + spring-boot-autoconfigure + ${spring.boot.version} + + + + + org.springframework + spring-core + ${spring.version} + + + org.springframework + spring-aop + ${spring.version} + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-jdbc + ${spring.version} + + + org.springframework + spring-tx + ${spring.version} + + + org.springframework.data + spring-data-jpa + ${spring.data.version} + + + org.slf4j + slf4j-api + + + + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + org.flywaydb + flyway-core + ${flyway.version} + + + + + com.h2database + h2 + ${h2.version} + + diff --git a/src/main/java/net/locusworks/s3sync/Entry.java b/src/main/java/net/locusworks/s3sync/Entry.java deleted file mode 100644 index 99acab5..0000000 --- a/src/main/java/net/locusworks/s3sync/Entry.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.locusworks.s3sync; - -import net.locusworks.logger.ApplicationLogger; -import net.locusworks.logger.ApplicationLoggerFactory; -import net.locusworks.logger.ApplicationLoggerInitializer; -import net.locusworks.logger.LogLevel; -import net.locusworks.s3sync.client.FileManager; -import net.locusworks.s3sync.client.S3Client; -import net.locusworks.s3sync.conf.ConfigurationManager; - -public class Entry { - - public static void main(String[] args) { - - ConfigurationManager conf = ConfigurationManager.getInstance(); - ApplicationLoggerFactory.init(new ApplicationLoggerInitializer() { - - @Override - public LogLevel initialize() { - return conf.getLogLevel(); - } - - }); - - ApplicationLogger logger = ApplicationLoggerFactory.getLogger(Entry.class); - - if (System.getenv("AWS_ACCESS_KEY_ID") == null) { - logger.error("AWS_ACCESS_KEY_ID is not set in environment variables"); - System.exit(-1); - } - - if (System.getenv("AWS_SECRET_ACCESS_KEY") == null) { - logger.error("AWS_SECRET_ACCESS_KEY is not set in environment variables"); - System.exit(-1); - } - - logger.info("Starting S3 Sync"); - - try (S3Client client = new S3Client(ConfigurationManager.getInstance())) { - FileManager manager = FileManager.newInstance(client); - client.syncFolder(); - manager.removeOrphanedFiles(); - } catch (Exception | Error e) { - logger.error(e); - System.exit(-1); - } - } -} diff --git a/src/main/java/net/locusworks/s3sync/S3SyncLauncher.java b/src/main/java/net/locusworks/s3sync/S3SyncLauncher.java new file mode 100644 index 0000000..9b55e31 --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/S3SyncLauncher.java @@ -0,0 +1,55 @@ +package net.locusworks.s3sync; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Component; +import net.locusworks.logger.ApplicationLogger; +import net.locusworks.logger.ApplicationLoggerFactory; +import net.locusworks.s3sync.client.S3Client; + + +@SpringBootApplication(scanBasePackages= {"net.locusworks.s3sync"}) +@ComponentScan("net.locusworks.s3sync") +@Component +@EnableScheduling +public class S3SyncLauncher implements ApplicationRunner { + + @Autowired + private S3Client client; + + public static void main(String[] args) { + new SpringApplicationBuilder(S3SyncLauncher.class).headless(false).run(args); + } + + @Override + public void run(ApplicationArguments args) throws Exception { + + ApplicationLogger logger = ApplicationLoggerFactory.getLogger(S3SyncLauncher.class); + + if (System.getenv("AWS_ACCESS_KEY_ID") == null) { + logger.error("AWS_ACCESS_KEY_ID is not set in environment variables"); + System.exit(-1); + } + + if (System.getenv("AWS_SECRET_ACCESS_KEY") == null) { + logger.error("AWS_SECRET_ACCESS_KEY is not set in environment variables"); + System.exit(-1); + } + + logger.info("Starting S3 Sync"); + + try { + client.syncFolder(); + } catch (Exception | Error e) { + logger.error(e); + System.exit(-1); + } finally { + client.close(); + } + } +} diff --git a/src/main/java/net/locusworks/s3sync/client/FileDetail.java b/src/main/java/net/locusworks/s3sync/client/FileDetail.java deleted file mode 100644 index 658195e..0000000 --- a/src/main/java/net/locusworks/s3sync/client/FileDetail.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.locusworks.s3sync.client; - -public class FileDetail { - - private String file; - private String hash; - private boolean uploaded; - - public FileDetail() {} - /** - * @param file - * @param hash - * @param uploaded - */ - public FileDetail(String file, String hash, boolean uploaded) { - this.file = file; - this.hash = hash; - this.uploaded = uploaded; - } - /** - * @return the file - */ - public synchronized final String getFile() { - return file; - } - /** - * @param file the file to set - */ - public synchronized final void setFile(String file) { - this.file = file; - } - /** - * @return the hash - */ - public synchronized final String getHash() { - return hash; - } - /** - * @param hash the hash to set - */ - public synchronized final void setHash(String hash) { - this.hash = hash; - } - /** - * @return the uploaded - */ - public synchronized final boolean isUploaded() { - return uploaded; - } - /** - * @param uploaded the uploaded to set - */ - public synchronized final void setUploaded(boolean uploaded) { - this.uploaded = uploaded; - } - -} diff --git a/src/main/java/net/locusworks/s3sync/client/FileManager.java b/src/main/java/net/locusworks/s3sync/client/FileManager.java index 18b027b..02c86c6 100644 --- a/src/main/java/net/locusworks/s3sync/client/FileManager.java +++ b/src/main/java/net/locusworks/s3sync/client/FileManager.java @@ -1,130 +1,52 @@ package net.locusworks.s3sync.client; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; +import java.util.Date; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang3.StringUtils; -import com.amazonaws.services.s3.model.S3Object; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import net.locusworks.s3sync.database.entities.FileInfo; +import net.locusworks.s3sync.database.repos.FileInfoRepository; import net.locusworks.logger.ApplicationLogger; import net.locusworks.logger.ApplicationLoggerFactory; -public class FileManager implements AutoCloseable { + +@Service +public class FileManager { private ApplicationLogger logger = ApplicationLoggerFactory.getLogger(FileManager.class); - - public static final String FILE_CSV = "upload.csv"; - - private Map detailMap; - private Set s3Files; + @Autowired + private FileInfoRepository fileInfoRepo; - private S3Client client; + public static final String S3SYNC_DB_FILE = "s3sync.mv.db"; - private String bucket; - - private static FileManager instance; - - private FileManager(S3Client client) throws IOException { - detailMap = new LinkedHashMap(); - s3Files = new HashSet(); - this.client = client; - this.bucket = client.getBucket(); - readFile(); + public FileInfo addEntry(Path file, String hash, boolean uploaded) { + return addEntry(new FileInfo(null, file.getFileName().toString(), file.getParent().toString(), hash, new Date(), uploaded)); } - private void readFile() throws IOException { + public FileInfo addEntry(FileInfo fd) { + logger.debug("Saving file: " + fd); + if (fd.getUploadDate() == null) fd.setUploadDate(new Date()); + return fileInfoRepo.save(fd); + } + + public FileInfo getFileDetail(Path file, String relativePath) throws IOException { + String sha1 = DigestUtils.sha1Hex(Files.newInputStream(file)); - S3Object hashFile = client.getObject(bucket, FILE_CSV); - if (hashFile == null) return; - Path file = Paths.get(FILE_CSV); + FileInfo fi = fileInfoRepo.findByFileHash(sha1); - client.downloadFile(FILE_CSV, file); - s3Files = client.getFileList(); - if (Files.notExists(file)) return; - - try(BufferedReader reader = Files.newBufferedReader(file)) { - String line = null; - while((line = reader.readLine()) != null) { - String[] values = line.split(","); - if (values.length != 3) { - logger.warn("Invalid entry detected: " + line); - continue; - } - - FileDetail fd = new FileDetail(values[0], values[1], Boolean.valueOf(values[2])); - detailMap.put(values[0], fd); - } + if (fi == null) { + fi = new FileInfo(); + fi.setFileHash(sha1); + fi.setFilePath(relativePath); + fi.setFileName(file.getFileName().toString()); + fi.setUploaded(false); } - } - - private void saveFile() throws IOException { - try(BufferedWriter writer = Files.newBufferedWriter(Paths.get(FILE_CSV))) { - writer.write("FILE_NAME,HASH,STATUS\n"); - for(FileDetail v : detailMap.values()) { - writer.write(String.format("%s,%s,%s%n", v.getFile(), v.getHash(), v.isUploaded())); - }; - } - } - - public boolean addEntry(String file, String hash, boolean uploaded) { - return addEntry(new FileDetail(file, hash, uploaded)); - } - - public boolean addEntry(FileDetail fd) { - return detailMap.put(fd.getFile(), fd) != null; - } - - public FileDetail getFileDetail(Path path, String key) throws IOException { - boolean newFile = false; - FileDetail fd = null; - if (detailMap.containsKey(key)) { - fd = detailMap.get(key); - } else { - newFile = true; - fd = new FileDetail(key, DigestUtils.sha1Hex(Files.newInputStream(path)), false); - } - - String sha1 = newFile ? fd.getHash() : DigestUtils.sha1Hex(Files.newInputStream(path)); - if (sha1.equals(fd.getHash()) && s3Files.contains(key)) { - fd.setUploaded(true); - s3Files.remove(key); - } else { - fd.setUploaded(false); - } - - return fd; - } - - public void removeOrphanedFiles() { - client.removeFiles(s3Files); - } - - @Override - public void close() throws Exception { - saveFile(); - client.uploadFile(Paths.get(FILE_CSV)); - Files.deleteIfExists(Paths.get(FILE_CSV)); - } - - public static FileManager newInstance(S3Client client) throws IOException { - instance = new FileManager(client); - return instance; - } - - public static FileManager getInstance() throws IOException { - if (instance == null || instance.client == null || StringUtils.isBlank(instance.bucket)) { - throw new IOException("a call to newInstance() was not made. Please call newInstance first"); - } - return instance; + return fi; } } diff --git a/src/main/java/net/locusworks/s3sync/client/S3Client.java b/src/main/java/net/locusworks/s3sync/client/S3Client.java index 0a90d32..e6e3ffa 100644 --- a/src/main/java/net/locusworks/s3sync/client/S3Client.java +++ b/src/main/java/net/locusworks/s3sync/client/S3Client.java @@ -7,6 +7,9 @@ import java.nio.file.Path; import java.util.Comparator; import java.util.HashSet; import java.util.Set; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.regions.Regions; @@ -19,31 +22,33 @@ import com.amazonaws.services.s3.transfer.Download; import com.amazonaws.services.s3.transfer.TransferManager; import com.amazonaws.services.s3.transfer.TransferManagerBuilder; import com.amazonaws.services.s3.transfer.Upload; -import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; import net.locusworks.logger.ApplicationLogger; import net.locusworks.logger.ApplicationLoggerFactory; import net.locusworks.s3sync.conf.ConfigurationManager; +import net.locusworks.s3sync.conf.TransferType; +import net.locusworks.s3sync.database.entities.FileInfo; +import net.locusworks.s3sync.scp.AbstractSshMessage; +import net.locusworks.s3sync.scp.LocalFileMessage; import net.locusworks.s3sync.scp.ScpFromMessage; -public class S3Client implements AutoCloseable { +@Component +public class S3Client { private ApplicationLogger logger = ApplicationLoggerFactory.getLogger(S3Client.class); private AmazonS3 s3Client; - - private String bucket; - private String remoteFolder; - private String host; - private String identity; - private String user; - - private Integer port; - - private Path localFolder; - public S3Client(ConfigurationManager conf) { + @Autowired + private ConfigurationManager conf; + + @Autowired + private FileManager fileManager; + + public S3Client() {} + + @PostConstruct + private void init() { String region = conf.getRegion(); this.s3Client = AmazonS3ClientBuilder.standard().withRegion(Regions.fromName(region)).build(); Bucket bucket = s3Client.listBuckets().stream() @@ -54,50 +59,40 @@ public class S3Client implements AutoCloseable { System.exit(-1); } logger.info("Found Bucket: %s", bucket); - this.bucket = conf.getBucketName(); - this.remoteFolder = conf.getRemoteFolder(); - this.localFolder = conf.getLocalFolder(); - this.host = conf.getHost(); - this.identity = conf.getIdentity(); - this.user = conf.getUser(); - this.port = conf.getPort(); } public String getBucket() { - return this.bucket; + return this.conf.getBucketName(); } - public void uploadFile(Path file) { + public void uploadFile(Path file, Path localFolder) { TransferManager xferMgr = TransferManagerBuilder.standard().withS3Client(s3Client).build(); - uploadFile(xferMgr, file); + uploadFile(xferMgr, file, localFolder); xferMgr.shutdownNow(false); } - public void uploadFile(TransferManager xferMgr, Path file) { + public void uploadFile(TransferManager xferMgr, Path file, Path localFolder) { boolean xferMgrNull = xferMgr == null; xferMgr = !xferMgrNull ? xferMgr : TransferManagerBuilder.standard().withS3Client(s3Client).build(); - FileDetail fd = null; + FileInfo fd = null; try { - String key = getPath(file); - fd = FileManager.getInstance().getFileDetail(file, key); - if (fd.isUploaded()) return; + String key = getPath(file, localFolder); + fd = fileManager.getFileDetail(file, key); + if (fd.getUploaded()) return; logger.info("Uploading file: %s", file); - Upload xfer = xferMgr.upload(bucket, key, file.toFile()); + Upload xfer = xferMgr.upload(getBucket(), key, file.toFile()); xfer.waitForCompletion(); fd.setUploaded(true); - FileManager.getInstance().addEntry(fd); logger.info("Done uploading %s", file); } catch (AmazonClientException | InterruptedException | IOException e) { if (fd != null) { fd.setUploaded(false); - try { - FileManager.getInstance().addEntry(fd); - } catch (IOException e1) { - logger.error("Unable to save file to file manager: " + e1.getMessage()); - } } logger.error(e.getMessage()); } finally { + if (fd != null) { + fileManager.addEntry(fd); + } if (xferMgrNull) { xferMgr.shutdownNow(false); } @@ -115,7 +110,7 @@ public class S3Client implements AutoCloseable { xferMgr = !xferMgrNull ? xferMgr : TransferManagerBuilder.standard().withS3Client(s3Client).build(); try { logger.info("Downloading file: %s", file); - Download xfer = xferMgr.download(bucket, key, file.toFile()); + Download xfer = xferMgr.download(getBucket(), key, file.toFile()); xfer.waitForCompletion(); logger.info("Done downloading %s", file); } catch (AmazonClientException | InterruptedException e) { @@ -129,23 +124,23 @@ public class S3Client implements AutoCloseable { public void syncFolder() throws IOException, JSchException { TransferManager xferMgr = TransferManagerBuilder.standard().withS3Client(s3Client).build(); - + Path localFolder = conf.getLocalFolder(); if (Files.notExists(localFolder)) { logger.info("Creating local folder: " + localFolder); Files.createDirectory(localFolder); } + + AbstractSshMessage aMsg = conf.getTransferType() == TransferType.REMOTE ? + new ScpFromMessage(true, ScpFromMessage.newSession(conf.getIdentity(), conf.getUser(), conf.getHost(), conf.getPort()), conf.getRemoteFolder(), localFolder, true) : + new LocalFileMessage(localFolder); - JSch.setConfig("StrictHostKeyChecking", "no"); - JSch jsch = new JSch(); - jsch.addIdentity(identity); - Session session = jsch.getSession(user, host, port); - session.connect(); - try(ScpFromMessage msg = new ScpFromMessage(true, session, remoteFolder, localFolder, true); FileManager manager = FileManager.getInstance()) { + try(AbstractSshMessage msg = aMsg) { msg.fileCallback(p -> { if (Files.isRegularFile(p)) { - uploadFile(xferMgr, p); + uploadFile(xferMgr, p, localFolder); try { - Files.delete(p); + if (conf.deleteLocal()) + Files.delete(p); } catch (Exception ex) { logger.warn("Unable to delete local file %s: %s", p, ex.getMessage()); } @@ -157,10 +152,11 @@ public class S3Client implements AutoCloseable { } xferMgr.shutdownNow(false); - Files.walk(localFolder) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); + if (conf.deleteLocal()) + Files.walk(localFolder) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); } public S3Object getObject(String bucketName, String key) { @@ -189,13 +185,12 @@ public class S3Client implements AutoCloseable { } } - private String getPath(Path file) { + private String getPath(Path file, Path localFolder) { if (file.getParent() == null) return file.getFileName().toString(); Path relative = localFolder.relativize(file); return relative.toString().replace("\\", "/"); } - @Override public void close() throws Exception { if (s3Client != null) { s3Client.shutdown(); diff --git a/src/main/java/net/locusworks/s3sync/conf/ConfigurationManager.java b/src/main/java/net/locusworks/s3sync/conf/ConfigurationManager.java index 44646f8..7b6559a 100644 --- a/src/main/java/net/locusworks/s3sync/conf/ConfigurationManager.java +++ b/src/main/java/net/locusworks/s3sync/conf/ConfigurationManager.java @@ -5,19 +5,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; +import javax.annotation.PostConstruct; +import org.springframework.stereotype.Component; import net.locusworks.logger.LogLevel; +@Component public class ConfigurationManager { private static final String CONF_FILE= "s3.properties"; private Properties conf; - private static ConfigurationManager confManager; - - private ConfigurationManager() { - init(); - } - + @PostConstruct private void init() { try { Properties defaultProps = PropertiesManager.loadConfiguration(ConfigurationManager.class, CONF_FILE); @@ -38,6 +36,14 @@ public class ConfigurationManager { } } + public TransferType getTransferType() { + return TransferType.getEnum(conf.getProperty("transferType")); + } + + public boolean deleteLocal() { + return Boolean.getBoolean(conf.getProperty("deleteLocalFiles")); + } + public String getRegion() { return conf.getProperty("region"); } @@ -74,11 +80,4 @@ public class ConfigurationManager { return LogLevel.getEnum(conf.getProperty("logLevel")); } - public static ConfigurationManager getInstance() { - if (confManager == null) { - confManager = new ConfigurationManager(); - } - return confManager; - } - } diff --git a/src/main/java/net/locusworks/s3sync/conf/TransferType.java b/src/main/java/net/locusworks/s3sync/conf/TransferType.java new file mode 100644 index 0000000..0d61f0a --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/conf/TransferType.java @@ -0,0 +1,15 @@ +package net.locusworks.s3sync.conf; + +public enum TransferType { + + REMOTE, + LOCAL; + + public static TransferType getEnum(String value) { + for (TransferType s : values()) { + if (s.name().toLowerCase().equals(value.toLowerCase().trim())) return s; + } + throw new RuntimeException("No transfer type of " + value); + } + +} diff --git a/src/main/java/net/locusworks/s3sync/database/S3SyncBeanConfiguration.java b/src/main/java/net/locusworks/s3sync/database/S3SyncBeanConfiguration.java new file mode 100644 index 0000000..37bef00 --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/database/S3SyncBeanConfiguration.java @@ -0,0 +1,195 @@ +package net.locusworks.s3sync.database; + +import java.util.HashSet; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.flywaydb.core.api.logging.Log; +import org.flywaydb.core.api.logging.LogCreator; +import org.flywaydb.core.api.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import net.locusworks.logger.ApplicationLogger; +import net.locusworks.logger.ApplicationLoggerFactory; +import net.locusworks.logger.ApplicationLoggerInitializer; +import net.locusworks.logger.LogLevel; +import net.locusworks.s3sync.conf.ConfigurationManager; + +@Configuration(value="net.locusworks.s3sync.database.S3SyncBeanConfiguration") +@EnableTransactionManagement +@EnableJpaRepositories( + basePackages = {"net.locusworks.s3sync.database.repos"}, + entityManagerFactoryRef = "s3syncEntityManagerFactory", + transactionManagerRef = "s3syncTransactionManager" +) +public class S3SyncBeanConfiguration { + + public static final String PERSISTENCE_UNIT = "s3sync_pu"; + + public static final HashSet cacheNames = new HashSet<>(); + + private static final String[] PACKAGES_TO_SCAN = {"net.locusworks.s3sync.database.entities", "net.locusworks.s3sync.database.repos"}; + + private static final Properties hibernateProperties; + static { + hibernateProperties = new Properties(); + hibernateProperties.setProperty("hibernate.connection.zeroDateTimeBehavior", "convertToNull"); + hibernateProperties.setProperty("hibernate.dbcp.maxActive", "50"); + hibernateProperties.setProperty("hibernate.dbcp.maxIdle", "10"); + hibernateProperties.setProperty("hibernate.dbcp.maxWait", "5000"); + hibernateProperties.setProperty("hibernate.jdbc.batch_size property", "50"); + hibernateProperties.setProperty("hibernate.connection.charSet", "UTF-8"); + hibernateProperties.setProperty("hibernate.connection.characterEncoding", "UTF-8"); + hibernateProperties.setProperty("hibernate.connection.useUnicode", "true"); + } + + @Autowired + private DataSource dataSource; + + /** + * Create the entity manager factory bean used in the database connection + * @return entityManagerFactoryBean + */ + @Primary + @Bean + @DependsOn("flyway") + public LocalContainerEntityManagerFactoryBean s3syncEntityManagerFactory() { + LocalContainerEntityManagerFactoryBean lef = new LocalContainerEntityManagerFactoryBean(); + lef.setDataSource(dataSource); + lef.setJpaVendorAdapter(s3syncJpaVendorAdapter()); + lef.setPackagesToScan(PACKAGES_TO_SCAN); + lef.setJpaProperties(hibernateProperties); + lef.setPersistenceUnitName(PERSISTENCE_UNIT); + return lef; + } + + /** + * Create the session factory bean + * @return sessionFactoryBean + */ + @Bean + public LocalSessionFactoryBean s3syncSessionFactory() { + LocalSessionFactoryBean sessionBean = new LocalSessionFactoryBean(); + sessionBean.setDataSource(dataSource); + sessionBean.setPackagesToScan(PACKAGES_TO_SCAN); + sessionBean.setHibernateProperties(hibernateProperties); + return sessionBean; + } + + /** + * Create the JPA Vendor adaptor bean that is used in the entity manager factory + * @return jpaVendorAdaptor + */ + @Primary + @Bean + public JpaVendorAdapter s3syncJpaVendorAdapter() { + HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter(); + hibernateJpaVendorAdapter.setShowSql(false); + hibernateJpaVendorAdapter.setGenerateDdl(false); + hibernateJpaVendorAdapter.setDatabase(Database.H2); + return hibernateJpaVendorAdapter; + } + + /** + * Create the transaction manager bean + * @return transactionManager + */ + @Primary + @Bean + public PlatformTransactionManager s3syncTransactionManager() { + JpaTransactionManager manager = new JpaTransactionManager(); + manager.setEntityManagerFactory(s3syncEntityManagerFactory().getObject()); + return new JpaTransactionManager(); + } + + @Bean(name="flyway", initMethod="migrate") + @DependsOn({"initializer"}) + public Flyway flyway() { + LogFactory.setLogCreator(new FlywayLogCreator()); + FluentConfiguration fc = Flyway.configure(); + fc.schemas("s3sync"); + fc.table("_flyway_migration"); + fc.locations("database/migration"); + fc.outOfOrder(false); + fc.dataSource(dataSource); + + return fc.load(); + } + + @Bean(name="initializer") + public Boolean initializer(ConfigurationManager confManager) { + ApplicationLoggerFactory.init(new ApplicationLoggerInitializer() { + + @Override + public LogLevel initialize() { + return confManager.getLogLevel(); + } + }); + return true; + } + + private class FlywayLogCreator implements LogCreator { + + @Override + public Log createLogger(Class clazz) { + return new FlywayLog(clazz); + } + } + + private class FlywayLog implements Log { + + private ApplicationLogger logger; + + public FlywayLog(Class clazz) { + logger = ApplicationLoggerFactory.getLogger(clazz); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String message) { + logger.debug(message); + } + + @Override + public void info(String message) { + logger.info(message); + } + + @Override + public void warn(String message) { + logger.warn(message); + } + + @Override + public void error(String message) { + logger.error(message); + } + + @Override + public void error(String message, Exception e) { + logger.error(message, e); + } + + } + +} diff --git a/src/main/java/net/locusworks/s3sync/database/S3SyncDataSource.java b/src/main/java/net/locusworks/s3sync/database/S3SyncDataSource.java new file mode 100644 index 0000000..717be7c --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/database/S3SyncDataSource.java @@ -0,0 +1,46 @@ +package net.locusworks.s3sync.database; + +import javax.sql.DataSource; + +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import net.locusworks.logger.ApplicationLogger; +import net.locusworks.logger.ApplicationLoggerFactory; + +@Primary +@Component +public class S3SyncDataSource { + + private static final String DRIVER = "org.h2.Driver"; + private static final String JNDI_STRING = "jdbc:h2:file:./s3sync;MODE=MYSQL;DATABASE_TO_UPPER=false"; + + /** + * Create the data source bean + * @return the data source bean + * @throws Exception + */ + @Bean + public DataSource dataSource() throws Exception { + return getDataSource(); + } + + + private DataSource getDataSource() throws Exception { + ApplicationLogger logger = ApplicationLoggerFactory.getLogger(this.getClass()); + + String url = JNDI_STRING; + + logger.debug(String.format("Database connection string: %s", url)); + + return DataSourceBuilder + .create() + .username("sa") + .password("") + .url(url) + .driverClassName(DRIVER) + .build(); + } + +} diff --git a/src/main/java/net/locusworks/s3sync/database/entities/FileInfo.java b/src/main/java/net/locusworks/s3sync/database/entities/FileInfo.java new file mode 100644 index 0000000..65494c1 --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/database/entities/FileInfo.java @@ -0,0 +1,183 @@ +package net.locusworks.s3sync.database.entities; + +import java.io.Serializable; +import java.util.Date; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name ="file_info", catalog="S3SYNC", schema="s3sync") +@NamedQueries({ + @NamedQuery(name = "FileInfo.findAll", query="SELECT f FROM FileInfo f") +}) +public class FileInfo implements Serializable { + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "ID") + private Long id; + + @Basic(optional = false) + @Column(name = "FILE_NAME") + private String fileName; + + @Basic(optional = false) + @Column(name = "FILE_HASH") + private String fileHash; + + @Basic(optional = false) + @Column(name = "FILE_PATH") + private String filePath; + + @Basic(optional = true) + @Column(name = "UPLOAD_DATE") + @Temporal(TemporalType.TIMESTAMP) + private Date uploadDate; + + @Basic(optional = false) + @Column(name = "UPLOADED") + private Boolean uploaded; + + public FileInfo() { + } + + public FileInfo(Long id) { + this.id = id; + } + + /** + * @param id + * @param fileName + * @param fileHash + * @param fileType + * @param uploadDate + * @param uploaded + */ + public FileInfo(Long id, String fileName, String fileHash, String filePath, Date uploadDate, Boolean uploaded) { + this.id = id; + this.fileName = fileName; + this.fileHash = fileHash; + this.filePath = filePath; + this.uploadDate = uploadDate; + this.uploaded = uploaded; + } + + /** + * @return the id + */ + public synchronized final Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public synchronized final void setId(Long id) { + this.id = id; + } + + /** + * @return the fileName + */ + public synchronized final String getFileName() { + return fileName; + } + + /** + * @param fileName the fileName to set + */ + public synchronized final void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * @return the fileHash + */ + public synchronized final String getFileHash() { + return fileHash; + } + + /** + * @param fileHash the fileHash to set + */ + public synchronized final void setFileHash(String fileHash) { + this.fileHash = fileHash; + } + + /** + * @return the fileType + */ + public synchronized final String getFilePath() { + return filePath; + } + + /** + * @param fileType the fileType to set + */ + public synchronized final void setFilePath(String filePath) { + this.filePath = filePath; + } + + /** + * @return the uploadDate + */ + public synchronized final Date getUploadDate() { + return uploadDate; + } + + /** + * @param uploadDate the uploadDate to set + */ + public synchronized final void setUploadDate(Date uploadDate) { + this.uploadDate = uploadDate; + } + + /** + * @return the uploaded + */ + public synchronized final Boolean getUploaded() { + return uploaded; + } + + /** + * @param uploaded the uploaded to set + */ + public synchronized final void setUploaded(Boolean uploaded) { + this.uploaded = uploaded; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof FileInfo)) { + return false; + } + FileInfo other = (FileInfo) object; + if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "net.locusworks.battletech.s3sync.entities.FileInfo[ id=" + id + " ]"; + } +} diff --git a/src/main/java/net/locusworks/s3sync/database/repos/FileInfoRepository.java b/src/main/java/net/locusworks/s3sync/database/repos/FileInfoRepository.java new file mode 100644 index 0000000..93127de --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/database/repos/FileInfoRepository.java @@ -0,0 +1,26 @@ +package net.locusworks.s3sync.database.repos; + +import javax.transaction.Transactional; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +import net.locusworks.s3sync.database.entities.FileInfo; + +/** + * + * @author isaac + */ +public interface FileInfoRepository extends CrudRepository { + + FileInfo findByFileHash(String fileHash); + + boolean existsByFileHash(String fileHash); + + @Modifying + @Transactional + @Query("DELETE FROM FileInfo fh WHERE fh.fileHash = ?1") + void deleteByFileHash(String fileHash); + +} diff --git a/src/main/java/net/locusworks/s3sync/scp/AbstractSshMessage.java b/src/main/java/net/locusworks/s3sync/scp/AbstractSshMessage.java index 214baf4..ac1c015 100644 --- a/src/main/java/net/locusworks/s3sync/scp/AbstractSshMessage.java +++ b/src/main/java/net/locusworks/s3sync/scp/AbstractSshMessage.java @@ -3,9 +3,9 @@ package net.locusworks.s3sync.scp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Path; import java.text.NumberFormat; - - +import java.util.function.Consumer; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.ChannelSftp; @@ -15,15 +15,17 @@ import com.jcraft.jsch.SftpProgressMonitor; import net.locusworks.logger.ApplicationLogger; import net.locusworks.logger.LogLevel; -public abstract class AbstractSshMessage { +public abstract class AbstractSshMessage implements AutoCloseable { private ApplicationLogger logger; private static final double ONE_SECOND = 1000.0; - protected Session session; + private Session session; private final boolean verbose; private final boolean compressed; + + protected Consumer fileCallback; /** * Constructor for AbstractSshMessage @@ -199,6 +201,27 @@ public abstract class AbstractSshMessage { protected final Session getSession() { return session; } + + protected void connect() throws JSchException { + if (session != null && !session.isConnected()) session.connect(); + } + + protected void disconnect() { + if (session != null && session.isConnected()) session.disconnect(); + } + + public Consumer fileCallback(Consumer callback) { + this.fileCallback = callback; + return this.fileCallback; + } + + @Override + public void close() throws Exception { + if (session != null && session.isConnected()) { + session.disconnect(); + session = null; + } + } /** * Track progress every 10% if 100kb < filesize < 1Mb. For larger diff --git a/src/main/java/net/locusworks/s3sync/scp/LocalFileMessage.java b/src/main/java/net/locusworks/s3sync/scp/LocalFileMessage.java new file mode 100644 index 0000000..7e5adf1 --- /dev/null +++ b/src/main/java/net/locusworks/s3sync/scp/LocalFileMessage.java @@ -0,0 +1,29 @@ +package net.locusworks.s3sync.scp; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import com.jcraft.jsch.JSchException; +import net.locusworks.logger.ApplicationLoggerFactory; +import net.locusworks.logger.LogLevel; + +public class LocalFileMessage extends AbstractSshMessage { + + private Path syncFolder; + + public LocalFileMessage(Path localFolder) { + super(true, false, null, ApplicationLoggerFactory.getLogger(LocalFileMessage.class)); + this.syncFolder = localFolder; + } + + @Override + public void execute() throws IOException, JSchException { + Files.walk(syncFolder) + .filter(f -> Files.isRegularFile(f)) + .forEach(f -> { + log("Found File: " + f, LogLevel.DEBUG); + if (fileCallback != null) fileCallback.accept(syncFolder.resolve(f)); + }); + } + +} diff --git a/src/main/java/net/locusworks/s3sync/scp/ScpFromMessage.java b/src/main/java/net/locusworks/s3sync/scp/ScpFromMessage.java index f8c07bd..43a0932 100644 --- a/src/main/java/net/locusworks/s3sync/scp/ScpFromMessage.java +++ b/src/main/java/net/locusworks/s3sync/scp/ScpFromMessage.java @@ -25,9 +25,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.attribute.FileTime; -import java.util.function.Consumer; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; @@ -39,7 +37,7 @@ import net.locusworks.logger.ApplicationLoggerFactory; import net.locusworks.logger.LogLevel; import net.locusworks.logger.ApplicationLogger; -public class ScpFromMessage extends AbstractSshMessage implements AutoCloseable { +public class ScpFromMessage extends AbstractSshMessage { private static ApplicationLogger logger = ApplicationLoggerFactory.getLogger(ScpFromMessage.class); @@ -52,8 +50,6 @@ public class ScpFromMessage extends AbstractSshMessage implements AutoCloseable private boolean isRecursive = false; private boolean preserveLastModified = false; - private Consumer fileCallback; - /** * Constructor for ScpFromMessage * @param session the ssh session to use @@ -134,11 +130,6 @@ public class ScpFromMessage extends AbstractSshMessage implements AutoCloseable this.preserveLastModified = preserveLastModified; } - public Consumer fileCallback(Consumer callback) { - this.fileCallback = callback; - return this.fileCallback; - } - /** * Carry out the transfer. * @throws IOException on i/o errors @@ -146,6 +137,8 @@ public class ScpFromMessage extends AbstractSshMessage implements AutoCloseable */ @Override public void execute() throws IOException, JSchException { + connect(); + String command = "scp -f "; if (isRecursive) { command += "-r "; @@ -316,26 +309,12 @@ public class ScpFromMessage extends AbstractSshMessage implements AutoCloseable } return index < 0 ? "" : remoteFile.substring(0, index + 1); } - - @Override - public void close() throws Exception { - if (session != null && session.isConnected()) { - session.disconnect(); - session = null; - } - } - public static void main(String[] args) throws Exception { + public static Session newSession(String identity, String user, String host, Integer port) throws JSchException { JSch.setConfig("StrictHostKeyChecking", "no"); JSch jsch = new JSch(); - jsch.addIdentity(String.format("%s/.ssh/id_rsa",System.getProperty("user.home"))); - Session session = jsch.getSession("iparenteau", "s3sync.locusworks.net", 22); - session.connect(); - try(ScpFromMessage msg = new ScpFromMessage(true, session, "logs", Paths.get("downloads"), true)) { - msg.fileCallback(p -> { - System.out.println(p); - }); - msg.execute(); - } + jsch.addIdentity(identity); + Session session = jsch.getSession(user, host, port); + return session; } } diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..6df6413 --- /dev/null +++ b/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,8 @@ + + + + org.hibernate.ejb.HibernatePersistence + net.locusworks.s3sync.database.entities.FileInfo + + + diff --git a/src/main/resources/database/migration/H2/Tables/V01_000_000__initial_tables.sql b/src/main/resources/database/migration/H2/Tables/V01_000_000__initial_tables.sql new file mode 100644 index 0000000..209d64b --- /dev/null +++ b/src/main/resources/database/migration/H2/Tables/V01_000_000__initial_tables.sql @@ -0,0 +1,10 @@ +CREATE TABLE s3sync.file_info ( + ID IDENTITY NOT NULL AUTO_INCREMENT, + FILE_NAME VARCHAR(1000) NOT NULL, + FILE_PATH VARCHAR(1000) NOT NULL, + FILE_HASH VARCHAR(40) NOT NULL, + UPLOAD_DATE TIMESTAMP, + UPLOADED BOOLEAN NOT NULL, + CONSTRAINT FILE_INFO_PK PRIMARY KEY (ID), + CONSTRAINT FILE_INFO_UN UNIQUE (FILE_HASH) +); diff --git a/src/main/resources/database/migration/H2/beforeMigrate.sql b/src/main/resources/database/migration/H2/beforeMigrate.sql new file mode 100644 index 0000000..aece435 --- /dev/null +++ b/src/main/resources/database/migration/H2/beforeMigrate.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS s3sync AUTHORIZATION sa; \ No newline at end of file diff --git a/src/main/resources/s3.properties b/src/main/resources/s3.properties index 3ad57bc..b3fc398 100644 --- a/src/main/resources/s3.properties +++ b/src/main/resources/s3.properties @@ -1,9 +1,11 @@ region=us-east-2 bucketName=locus2k-backup +transferType=remote remoteFolder=logs logLevel=INFO identity=${user.home}/.ssh/id_rsa host=s3sync.locusworks.net user=iparenteau port=22 -localFolder=Downloads \ No newline at end of file +localFolder=Downloads +deleteLocalFiles=true \ No newline at end of file