├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── smattme │ ├── EmailService.java │ ├── MysqlBaseService.java │ ├── MysqlExportService.java │ ├── MysqlImportService.java │ ├── TablesResponse.java │ ├── exceptions │ └── MysqlBackup4JException.java │ └── helpers │ └── MysqlExportServiceHelper.java └── test ├── java └── com │ └── smattme │ ├── MysqlBackup4JIntegrationTest.java │ └── MysqlBackup4JUnitTest.java └── resources ├── mysql_init.sql └── simplelogger.properties /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | build-and-test: 5 | docker: 6 | - image: cimg/openjdk:8.0 7 | steps: 8 | - setup_remote_docker: 9 | docker_layer_caching: true 10 | - checkout 11 | - run: 12 | name: Build 13 | command: mvn -B clean package 14 | 15 | workflows: 16 | build: 17 | jobs: 18 | - build-and-test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /src/test/java/com/smattme/MysqlExportServiceUnitTest.java 3 | /target/ 4 | /.idea/ 5 | .DS_Store 6 | /smatt_files/ 7 | /external/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Seun Matt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mysql-backup4j 2 | ============== 3 | 4 | [![SeunMatt](https://circleci.com/gh/SeunMatt/request-validator.svg?style=svg)](https://github.com/SeunMatt/mysql-backup4j) 5 | 6 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.smattme/mysql-backup4j/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.smattme/mysql-backup4j/badge.svg) 7 | 8 | mysql-backup4j is a library for programmatically exporting mysql databases 9 | and sending the zipped dump to email, Amazon S3, Google Drive or any other cloud storage of choice 10 | 11 | **It gives the developer access to the generated zip file and the generated SQL query string** 12 | for use in other part of the application. 13 | 14 | **It also provides a method for importing the SQL exported by the tool - programmatically.** 15 | 16 | Installation 17 | ============ 18 | The artifact is available on Maven Central and can be added to the project's pom.xml: 19 | 20 | ```xml 21 | 22 | com.smattme 23 | mysql-backup4j 24 | 1.3.0 25 | 26 | ``` 27 | 28 | The latest version can be found [here](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.smattme%22%20a%3A%22mysql-backup4j%22) 29 | 30 | Usage 31 | ===== 32 | The minimum configuration required for the library is the database name, username and password. 33 | 34 | However, if you want the backup file to be sent to your email automatically after backup, you must 35 | provide email configurations as well. 36 | 37 | ```java 38 | //required properties for exporting of db 39 | Properties properties = new Properties(); 40 | properties.setProperty(MysqlExportService.DB_NAME, "database-name"); 41 | properties.setProperty(MysqlExportService.DB_USERNAME, "root"); 42 | properties.setProperty(MysqlExportService.DB_PASSWORD, "root"); 43 | properties.setProperty(MysqlExportService.DB_HOST, "localhost"); 44 | properties.setProperty(MysqlExportService.DB_PORT, "3306"); 45 | 46 | //properties relating to email config 47 | properties.setProperty(MysqlExportService.EMAIL_HOST, "smtp.mailtrap.io"); 48 | properties.setProperty(MysqlExportService.EMAIL_PORT, "25"); 49 | properties.setProperty(MysqlExportService.EMAIL_USERNAME, "mailtrap-username"); 50 | properties.setProperty(MysqlExportService.EMAIL_PASSWORD, "mailtrap-password"); 51 | properties.setProperty(MysqlExportService.EMAIL_FROM, "test@smattme.com"); 52 | properties.setProperty(MysqlExportService.EMAIL_TO, "backup@smattme.com"); 53 | 54 | //optional email configs 55 | properties.setProperty(MysqlExportService.EMAIL_SSL_PROTOCOLS, "TLSv1.2"); 56 | properties.setProperty(MysqlExportService.EMAIL_SMTP_AUTH_ENABLED, "true"); 57 | properties.setProperty(MysqlExportService.EMAIL_START_TLS_ENABLED, "true"); 58 | 59 | //set the outputs temp dir 60 | properties.setProperty(MysqlExportService.TEMP_DIR, new File("external").getPath()); 61 | 62 | MysqlExportService mysqlExportService = new MysqlExportService(properties); 63 | mysqlExportService.export(); 64 | ``` 65 | 66 | Calling `mysqlExportService.export();` will export the database and save the dump temporarily in the configured `TEMP_DIR` 67 | 68 | If an email config is supplied, the dump will be sent as an attachment. Finally, when all operations are completed the 69 | temporary dir is cleared and deleted. 70 | 71 | If you want to get the generated backup file as a Java `File` object, you need to specify this property as part of the 72 | configuration: 73 | 74 | ```java 75 | //... 76 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_ZIP, "true"); 77 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_SQL_FILE, "true"); 78 | ``` 79 | 80 | and then you can call this method: 81 | 82 | ```java 83 | File file = mysqlExportService.getGeneratedZipFile(); 84 | ``` 85 | 86 | Finally, let's say for some reason you want the generated SQL string you can do this: 87 | 88 | ```java 89 | String generatedSql = mysqlExportService.getGeneratedSql(); 90 | ``` 91 | 92 | Other parameters are: 93 | 94 | ```java 95 | properties.setProperty(MysqlExportService.ADD_IF_NOT_EXISTS, "true"); 96 | properties.setProperty(MysqlExportService.JDBC_DRIVER_NAME, "com.mysql.cj.jdbc.Driver"); 97 | properties.setProperty(MysqlExportService.JDBC_CONNECTION_STRING, "jdbc:mysql://localhost:3306/database-name?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false"); 98 | ``` 99 | 100 | They are explained in a detailed manner in this [tutorial](https://smattme.com/blog/technology/how-to-backup-mysql-database-programmatically-using-mysql-backup4j) 101 | 102 | Importing a Database 103 | -------------------- 104 | To import a database, you need to use the ImportService like so: 105 | 106 | ```java 107 | String sql = new String(Files.readAllBytes(Paths.get("path/to/sql/dump/file.sql"))); 108 | 109 | boolean res = MysqlImportService.builder() 110 | .setDatabase("database-name") 111 | .setSqlString(sql) 112 | .setUsername("root") 113 | .setPassword("root") 114 | .setHost("localhost") 115 | .setPort("3306") 116 | .setDeleteExisting(true) 117 | .setDropExisting(true) 118 | .importDatabase(); 119 | 120 | assertTrue(res); 121 | ``` 122 | 123 | First get SQL as a String and then pass it to the import service with the right configurations. 124 | 125 | Alternatively, you can also use the `.setJdbcConnString(jdbcURL)` method on the import service. 126 | 127 | e.g. 128 | ```java 129 | boolean res = MysqlImportService.builder() 130 | .setSqlString(generatedSql) 131 | .setJdbcConnString("jdbc:mysql://localhost:3306/backup4j_test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false") 132 | .setUsername("db-username") 133 | .setPassword("db-password") 134 | .setDeleteExisting(true) 135 | .setDropExisting(true) 136 | .importDatabase(); 137 | ``` 138 | 139 | `setDeleteExisting(true)` will **delete all data** from existing tables in the target database. 140 | 141 | While `setDropExisting(true)` will **drop** the table. 142 | 143 | Supplying `false` to these functions will disable their respective actions. 144 | 145 | 146 | **NOTE: The import service is only guaranteed to work with SQL files generated by the export service of this library** 147 | 148 | CHANGELOG 149 | ========= 150 | v1.2.1 151 | - Raises a new runtime exception `MysqlBackup4JException` if the required properties are not configured 152 | 153 | Author 154 | ====== 155 | Seun Matt [smattme.com](https://smattme.com) with :green_heart: 156 | 157 | Contributions and Support 158 | ========================= 159 | If you want to create a new feature, though not compulsory, but it will be helpful to reach out to me first before proceeding. 160 | 161 | To avoid a scenario where you submit a PR for an issue that someone else is working on already. 162 | 163 | 164 | Tutorials / Articles 165 | ==================== 166 | - [https://smattme.com/blog/technology/how-to-backup-mysql-database-programmatically-using-mysql-backup4j](https://smattme.com/blog/technology/how-to-backup-mysql-database-programmatically-using-mysql-backup4j) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.smattme 8 | mysql-backup4j 9 | 1.3.0 10 | jar 11 | 12 | ${project.groupId}:${project.artifactId} 13 | 14 | This is a simple library for backing up mysql databases and sending to emails, cloud storage and so on. 15 | It also provide a method for programmatically, importing SQL queries generated during the export process, 16 | 17 | https://github.com/SeunMatt/mysql-backup4j 18 | 19 | 20 | 21 | MIT License 22 | http://www.opensource.org/licenses/mit-license.php 23 | 24 | 25 | 26 | 27 | 28 | Seun Matt 29 | smatt382@gmail.com 30 | SmattMe 31 | https://smattme.com 32 | 33 | 34 | 35 | 36 | scm:git:git://github.com/SeunMatt/mysql-backup4j.git 37 | scm:git:ssh://github.com:SeunMatt/mysql-backup4j.git 38 | https://github.com/SeunMatt/mysql-backup4j/tree/master 39 | 40 | 41 | 42 | 1.20.0 43 | 44 | 45 | 46 | 47 | 48 | org.testcontainers 49 | testcontainers-bom 50 | ${testcontainers.version} 51 | pom 52 | import 53 | 54 | 55 | 56 | 57 | 58 | 59 | mysql 60 | mysql-connector-java 61 | 8.0.33 62 | 63 | 64 | org.zeroturnaround 65 | zt-zip 66 | 1.17 67 | jar 68 | 69 | 70 | javax.mail 71 | mail 72 | 1.5.0-b01 73 | 74 | 75 | org.slf4j 76 | slf4j-api 77 | 1.7.25 78 | 79 | 80 | org.slf4j 81 | slf4j-simple 82 | 1.7.25 83 | test 84 | 85 | 86 | org.junit.jupiter 87 | junit-jupiter-api 88 | 5.7.0 89 | test 90 | 91 | 92 | org.testcontainers 93 | testcontainers 94 | test 95 | 96 | 97 | org.testcontainers 98 | junit-jupiter 99 | test 100 | 101 | 102 | org.testcontainers 103 | mysql 104 | test 105 | 106 | 107 | 108 | 109 | 110 | ossrh 111 | https://oss.sonatype.org/content/repositories/snapshots 112 | 113 | 114 | ossrh 115 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 116 | 117 | 118 | 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-compiler-plugin 124 | 3.1 125 | 126 | 1.8 127 | 1.8 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-surefire-plugin 133 | 3.0.0-M5 134 | 135 | 136 | com.smattme.MysqlBackup4JIntegrationTest 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-javadoc-plugin 143 | 3.2.0 144 | 145 | 146 | attach-javadocs 147 | 148 | jar 149 | 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-source-plugin 156 | 3.0.1 157 | 158 | 159 | attach-sources 160 | 161 | jar 162 | 163 | 164 | 165 | 166 | 167 | org.apache.maven.plugins 168 | maven-gpg-plugin 169 | 1.6 170 | 171 | 172 | sign-artifacts 173 | verify 174 | 175 | sign 176 | 177 | 178 | 179 | 180 | 181 | org.sonatype.plugins 182 | nexus-staging-maven-plugin 183 | 1.6.8 184 | true 185 | 186 | ossrh 187 | https://oss.sonatype.org/ 188 | false 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.mail.*; 7 | import javax.mail.internet.InternetAddress; 8 | import javax.mail.internet.MimeBodyPart; 9 | import javax.mail.internet.MimeMessage; 10 | import javax.mail.internet.MimeMultipart; 11 | import java.io.File; 12 | import java.util.Properties; 13 | 14 | /** 15 | * Created by seun_ on 25-Feb-18. 16 | * 17 | */ 18 | class EmailService { 19 | 20 | private String host = ""; 21 | private int port = 0; 22 | private String fromAdd = ""; 23 | private String toAdd = ""; 24 | private String username = ""; 25 | private String password = ""; 26 | private String subject = ""; 27 | private String msg = ""; 28 | private String sslProtocols = "TLSv1.2"; 29 | private String startTlsEnabled = "true"; 30 | private boolean smtpAuthEnabled = true; 31 | private File [] attachments; 32 | private Logger logger = LoggerFactory.getLogger(EmailService.class); 33 | private final String LOG_PREFIX = "java-mysql-exporter"; 34 | 35 | 36 | private EmailService() {} 37 | 38 | /** 39 | * This is used to instantiate this class and form a 40 | * builder pattern 41 | * @return EmailService a new instance of this class 42 | */ 43 | static EmailService builder() { 44 | return new EmailService(); 45 | } 46 | 47 | EmailService setHost(String host){ 48 | this.host = host; 49 | return this; 50 | } 51 | 52 | EmailService setPort(int port) { 53 | this.port = port; 54 | return this; 55 | } 56 | 57 | EmailService setFromAddress(String fromAdd) { 58 | this.fromAdd = fromAdd; 59 | return this; 60 | } 61 | 62 | EmailService setToAddress(String toAdd) { 63 | this.toAdd = toAdd; 64 | return this; 65 | } 66 | 67 | EmailService setUsername(String username) { 68 | this.username = username; 69 | return this; 70 | } 71 | 72 | EmailService setPassword(String password) { 73 | this.password = password; 74 | return this; 75 | } 76 | 77 | EmailService setSubject(String subject) { 78 | this.subject = subject; 79 | return this; 80 | } 81 | 82 | EmailService setMessage(String message) { 83 | this.msg = message; 84 | return this; 85 | } 86 | 87 | EmailService setAttachments(File[] files) { 88 | this.attachments = files; 89 | return this; 90 | } 91 | 92 | EmailService setSslProtocols(String sslProtocols) { 93 | this.sslProtocols = sslProtocols; 94 | return this; 95 | } 96 | 97 | EmailService setStartTlsEnabled(String startTlsEnabled) { 98 | this.startTlsEnabled = startTlsEnabled; 99 | return this; 100 | } 101 | 102 | EmailService setSmtpAuthEnabled(boolean smtpAuthEnabled) { 103 | this.smtpAuthEnabled = smtpAuthEnabled; 104 | return this; 105 | } 106 | 107 | /** 108 | * This will check if the necessary properties 109 | * are set for sending an email successfully 110 | * @return boolean 111 | */ 112 | private boolean isPropertiesSet() { 113 | return !this.host.isEmpty() && 114 | this.port > 0 && 115 | !this.username.isEmpty() && 116 | !this.password.isEmpty() && 117 | !this.toAdd.isEmpty() && 118 | !this.fromAdd.isEmpty() && 119 | !this.subject.isEmpty() && 120 | !this.msg.isEmpty() && 121 | this.attachments != null && this.attachments.length > 0; 122 | } 123 | 124 | 125 | /** 126 | * This function will send an email 127 | * and add the generated sql file as an attachment 128 | * @return boolean 129 | */ 130 | boolean sendMail() { 131 | 132 | if(!this.isPropertiesSet()) { 133 | logger.debug(LOG_PREFIX + ": Required Mail Properties are not set. Attachments will not be sent"); 134 | return false; 135 | } 136 | 137 | Properties prop = new Properties(); 138 | prop.put("mail.smtp.auth", true); 139 | prop.put("mail.smtp.starttls.enable", "true"); 140 | prop.put("mail.smtp.host", this.host); 141 | prop.put("mail.smtp.port", this.port); 142 | prop.put("mail.smtp.ssl.trust", host); 143 | prop.put("mail.smtp.ssl.protocols", sslProtocols); 144 | 145 | logger.debug(LOG_PREFIX + ": Mail properties set"); 146 | 147 | Session session = Session.getInstance(prop, new Authenticator() { 148 | @Override 149 | protected PasswordAuthentication getPasswordAuthentication() { 150 | return new PasswordAuthentication(username, password); 151 | } 152 | }); 153 | 154 | logger.debug(LOG_PREFIX + ": Mail Session Created"); 155 | 156 | try { 157 | // create a default mime message object 158 | Message message = new MimeMessage(session); 159 | message.setFrom(new InternetAddress(fromAdd)); 160 | message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toAdd)); 161 | message.setSubject(subject); 162 | 163 | // body part for message 164 | MimeBodyPart mimeBodyPart = new MimeBodyPart(); 165 | mimeBodyPart.setContent(msg, "text/html"); 166 | 167 | // body part for attachments 168 | MimeBodyPart attachmentBodyPart = new MimeBodyPart(); 169 | logger.debug(LOG_PREFIX + ": " + this.attachments.length + " attachments found"); 170 | for (File file: this.attachments) { 171 | attachmentBodyPart.attachFile(file); 172 | } 173 | 174 | // create a multipart to combine them together 175 | Multipart multipart = new MimeMultipart(); 176 | multipart.addBodyPart(mimeBodyPart); 177 | multipart.addBodyPart(attachmentBodyPart); 178 | 179 | //now set the multipart as the content of the message 180 | message.setContent(multipart); 181 | 182 | // send the message 183 | Transport.send(message); 184 | 185 | logger.debug(LOG_PREFIX + ": MESSAGE SENT SUCCESSFULLY"); 186 | 187 | return true; 188 | 189 | } catch (Exception e) { 190 | logger.debug(LOG_PREFIX + ": MESSAGE NOT SENT. " + e.getLocalizedMessage()); 191 | e.printStackTrace(); 192 | return false; 193 | } 194 | 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/MysqlBaseService.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import com.smattme.exceptions.MysqlBackup4JException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.sql.*; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Objects; 11 | 12 | /** 13 | * Created by seun_ on 01-Mar-18. 14 | * 15 | */ 16 | public class MysqlBaseService { 17 | 18 | private static Logger logger = LoggerFactory.getLogger(MysqlBaseService.class); 19 | 20 | static final String SQL_START_PATTERN = "-- start"; 21 | static final String SQL_END_PATTERN = "-- end"; 22 | 23 | /** 24 | * This is a utility function for connecting to a 25 | * database instance that's running on localhost at port 3306. 26 | * It will build a JDBC URL from the given parameters and use that to 27 | * obtain a connect from doConnect() 28 | * @param username database username 29 | * @param password database password 30 | * @param database database name 31 | * @param driverName the user supplied mysql connector driver class name. Can be empty 32 | * @return Connection 33 | * @throws ClassNotFoundException exception 34 | * @throws SQLException exception 35 | */ 36 | @Deprecated 37 | static Connection connect(String username, String password, String database, String driverName) throws ClassNotFoundException, SQLException { 38 | String url = "jdbc:mysql://localhost:3306/" + database + "?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false"; 39 | String driver = (Objects.isNull(driverName) || driverName.isEmpty()) ? "com.mysql.cj.jdbc.Driver" : driverName; 40 | return doConnect(driver, url, username, password); 41 | } 42 | 43 | public static Connection connect(String username, String password, String host, String port, String database, String driverName) throws ClassNotFoundException, SQLException { 44 | 45 | String url = String.format("jdbc:mysql://%s:%s/%s", host, port, database); 46 | url = url + "?useUnicode=true&useJDBCCompliantTimezoneShift=true" 47 | + "&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false"; 48 | 49 | String driver = (Objects.isNull(driverName) || driverName.isEmpty()) 50 | ? "com.mysql.cj.jdbc.Driver" 51 | : driverName; 52 | 53 | return doConnect(driver, url, username, password); 54 | } 55 | 56 | 57 | /** 58 | * This is a utility function that allows connecting 59 | * to a database instance identified by the provided jdbcURL 60 | * The connector driver name can be empty 61 | * @param username database username 62 | * @param password database password 63 | * @param jdbcURL the user supplied JDBC URL. It's used as is. So ensure you supply the right parameters 64 | * @param driverName the user supplied mysql connector driver class name 65 | * @return Connection 66 | * @throws ClassNotFoundException exception 67 | * @throws SQLException exception 68 | */ 69 | static Connection connectWithURL(String username, String password, String jdbcURL, String driverName) throws ClassNotFoundException, SQLException { 70 | String driver = (Objects.isNull(driverName) || driverName.isEmpty()) ? "com.mysql.cj.jdbc.Driver" : driverName; 71 | return doConnect(driver, jdbcURL, username, password); 72 | } 73 | 74 | /** 75 | * This will attempt to connect to a database using 76 | * the provided parameters. 77 | * On success it'll return the java.sql.Connection object 78 | * @param driver the class name for the mysql driver to use 79 | * @param url the url of the database 80 | * @param username database username 81 | * @param password database password 82 | * @return Connection 83 | * @throws SQLException exception 84 | * @throws ClassNotFoundException exception 85 | */ 86 | private static Connection doConnect(String driver, String url, String username, String password) throws SQLException, ClassNotFoundException { 87 | Class.forName(driver); 88 | Connection connection = DriverManager.getConnection(url, username, password); 89 | logger.debug("DB Connected Successfully"); 90 | return connection; 91 | } 92 | 93 | /** 94 | * This is a utility function to get the names of all 95 | * the tables and views that're in the database supplied 96 | * @param database the database name 97 | * @param stmt Statement object 98 | * @return TableResponse object containing the list of tables and views 99 | * @throws SQLException exception 100 | */ 101 | static TablesResponse getAllTablesAndViews(String database, Statement stmt) throws SQLException { 102 | 103 | List tables = new ArrayList<>(); 104 | List views = new ArrayList<>(); 105 | 106 | ResultSet rs; 107 | rs = stmt.executeQuery("SHOW TABLE STATUS FROM `" + database + "`;"); 108 | while ( rs.next() ) { 109 | String comment = rs.getString("Comment"); 110 | if("VIEW".equals(comment)) { 111 | views.add(rs.getString("Name")); 112 | } 113 | else { 114 | tables.add(rs.getString("Name")); 115 | } 116 | } 117 | 118 | return new TablesResponse(tables, views); 119 | } 120 | 121 | /** 122 | * This function is an helper function 123 | * that'll generate a DELETE FROM database.table 124 | * SQL to clear existing table 125 | * @param database database 126 | * @param table table 127 | * @return String sql to delete the all records from the table 128 | */ 129 | static String getEmptyTableSQL(String database, String table) { 130 | String safeDeleteSQL = "SELECT IF( \n" + 131 | "(SELECT COUNT(1) as table_exists FROM information_schema.tables \n" + 132 | "WHERE table_schema='" + database + "' AND table_name='" + table + "') > 1, \n" + 133 | "'DELETE FROM " + table + "', \n" + 134 | "'SELECT 1') INTO @DeleteSQL; \n" + 135 | "PREPARE stmt FROM @DeleteSQL; \n" + 136 | "EXECUTE stmt; DEALLOCATE PREPARE stmt; \n"; 137 | 138 | return "\n" + MysqlBaseService.SQL_START_PATTERN + "\n" + 139 | safeDeleteSQL + "\n" + 140 | "\n" + MysqlBaseService.SQL_END_PATTERN + "\n"; 141 | } 142 | 143 | /** 144 | * This function will extract the database name from the 145 | * supplied JDBC connection URL. 146 | * @param jdbcURL JDBC Connection URL 147 | * @return database name extracted from the connection URL 148 | * @exception MysqlBackup4JException if an invalid jdbcURL is supplied 149 | */ 150 | public static String extractDatabaseNameFromJDBCUrl(String jdbcURL) { 151 | 152 | if(jdbcURL == null || jdbcURL.isEmpty()) 153 | throw new MysqlBackup4JException("Null or Empty JDBC URL supplied: " + jdbcURL); 154 | 155 | //strip the extra properties from the URL 156 | String jdbcURLWithoutParams; 157 | if(jdbcURL.contains("?")) { 158 | jdbcURLWithoutParams = jdbcURL.substring(0, jdbcURL.indexOf("?")); 159 | } 160 | else { 161 | jdbcURLWithoutParams = jdbcURL; 162 | } 163 | 164 | return jdbcURLWithoutParams.substring(jdbcURLWithoutParams.lastIndexOf("/") + 1); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/MysqlExportService.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import com.smattme.exceptions.MysqlBackup4JException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.zeroturnaround.zip.ZipUtil; 7 | 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.sql.*; 12 | import java.text.SimpleDateFormat; 13 | import java.util.Date; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.Properties; 17 | 18 | import static com.smattme.helpers.MysqlExportServiceHelper.bytesToHex; 19 | 20 | /** 21 | * Created by seun_ on 24-Feb-18. 22 | * 23 | */ 24 | public class MysqlExportService { 25 | 26 | private Statement stmt; 27 | private String database; 28 | private String generatedSql = ""; 29 | private Logger logger = LoggerFactory.getLogger(getClass()); 30 | private final String LOG_PREFIX = "mysql-backup4j-export"; 31 | private String dirName = "mysql-backup4j-export-temp"; 32 | private String sqlFileName = ""; 33 | private String zipFileName = ""; 34 | private Properties properties; 35 | private File generatedZipFile; 36 | 37 | public static final String EMAIL_HOST = "EMAIL_HOST"; 38 | public static final String EMAIL_PORT = "EMAIL_PORT"; 39 | public static final String EMAIL_USERNAME = "EMAIL_USERNAME"; 40 | public static final String EMAIL_PASSWORD = "EMAIL_PASSWORD"; 41 | public static final String EMAIL_SUBJECT = "EMAIL_SUBJECT"; 42 | public static final String EMAIL_MESSAGE = "EMAIL_MESSAGE"; 43 | public static final String EMAIL_FROM = "EMAIL_FROM"; 44 | public static final String EMAIL_TO = "EMAIL_TO"; 45 | public static final String EMAIL_SSL_PROTOCOLS = "EMAIL_SSL_PROTOCOLS"; 46 | public static final String EMAIL_START_TLS_ENABLED = "EMAIL_START_TLS_ENABLED"; 47 | public static final String EMAIL_SMTP_AUTH_ENABLED = "EMAIL_SMTP_AUTH_ENABLED"; 48 | public static final String DB_NAME = "DB_NAME"; 49 | public static final String DB_USERNAME = "DB_USERNAME"; 50 | public static final String DB_PASSWORD = "DB_PASSWORD"; 51 | public static final String DB_HOST = "DB_HOST"; 52 | public static final String DB_PORT = "DB_PORT"; 53 | public static final String PRESERVE_GENERATED_ZIP = "PRESERVE_GENERATED_ZIP"; 54 | public static final String PRESERVE_GENERATED_SQL_FILE = "PRESERVE_GENERATED_SQL_FILE"; 55 | public static final String TEMP_DIR = "TEMP_DIR"; 56 | public static final String ADD_IF_NOT_EXISTS = "ADD_IF_NOT_EXISTS"; 57 | 58 | 59 | /** 60 | * @deprecated 61 | * This is deprecated in favour of the same option available 62 | * in the {@link MysqlImportService} class. 63 | */ 64 | public static final String DROP_TABLES = "DROP_TABLES"; 65 | 66 | 67 | /** 68 | * @deprecated 69 | * This is deprecated in favour of the same option available 70 | * in the {@link MysqlImportService} class. 71 | */ 72 | public static final String DELETE_EXISTING_DATA = "DELETE_EXISTING_DATA"; 73 | 74 | 75 | public static final String JDBC_CONNECTION_STRING = "JDBC_CONNECTION_STRING"; 76 | public static final String JDBC_DRIVER_NAME = "JDBC_DRIVER_NAME"; 77 | public static final String SQL_FILE_NAME = "SQL_FILE_NAME"; 78 | 79 | 80 | public MysqlExportService(Properties properties) { 81 | this.properties = properties; 82 | } 83 | 84 | /** 85 | * This function will check if the required minimum 86 | * properties are set for database connection and exporting 87 | * password is excluded here because it's possible to have a mysql database 88 | * user with no password 89 | * @return true if all required properties are present and false if otherwise 90 | */ 91 | private boolean isValidateProperties() { 92 | return properties != null && 93 | properties.containsKey(DB_USERNAME) && 94 | (properties.containsKey(DB_NAME) || properties.containsKey(JDBC_CONNECTION_STRING)); 95 | } 96 | 97 | /** 98 | * This function will check if all the minimum 99 | * required email properties are set, 100 | * that can facilitate sending of exported 101 | * sql to email 102 | * @return bool 103 | */ 104 | private boolean isEmailPropertiesSet() { 105 | return properties != null && 106 | properties.containsKey(EMAIL_HOST) && 107 | properties.containsKey(EMAIL_PORT) && 108 | properties.containsKey(EMAIL_USERNAME) && 109 | properties.containsKey(EMAIL_PASSWORD) && 110 | properties.containsKey(EMAIL_FROM) && 111 | properties.containsKey(EMAIL_TO); 112 | } 113 | 114 | /** 115 | * This function will return true 116 | * or false based on the availability 117 | * or absence of a custom output sql 118 | * file name 119 | * @return bool 120 | */ 121 | private boolean isSqlFileNamePropertySet(){ 122 | return properties != null && 123 | properties.containsKey(SQL_FILE_NAME); 124 | } 125 | 126 | /** 127 | * This will generate the SQL statement 128 | * for creating the table supplied in the 129 | * method signature 130 | * @param table the table concerned 131 | * @return String 132 | * @throws SQLException exception 133 | */ 134 | private String getTableInsertStatement(String table) throws SQLException { 135 | 136 | StringBuilder sql = new StringBuilder(); 137 | ResultSet rs; 138 | boolean addIfNotExists = Boolean.parseBoolean(properties.containsKey(ADD_IF_NOT_EXISTS) ? properties.getProperty(ADD_IF_NOT_EXISTS, "true") : "true"); 139 | 140 | 141 | if(table != null && !table.isEmpty()){ 142 | rs = stmt.executeQuery("SHOW CREATE TABLE " + "`" + table + "`;"); 143 | while ( rs.next() ) { 144 | String qtbl = rs.getString(1); 145 | String query = rs.getString(2); 146 | sql.append("\n\n--"); 147 | sql.append("\n").append(MysqlBaseService.SQL_START_PATTERN).append(" table dump : ").append(qtbl); 148 | sql.append("\n--\n\n"); 149 | 150 | if(addIfNotExists) { 151 | query = query.trim().replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); 152 | } 153 | 154 | sql.append(query).append(";\n\n"); 155 | } 156 | 157 | sql.append("\n\n--"); 158 | sql.append("\n").append(MysqlBaseService.SQL_END_PATTERN).append(" table dump : ").append(table); 159 | sql.append("\n--\n\n"); 160 | } 161 | 162 | return sql.toString(); 163 | } 164 | 165 | /** 166 | * this will generate the SQL statement to re-create 167 | * the supplied view 168 | * @param view the name of the View 169 | * @return an SQL to create the view 170 | * @throws SQLException on error 171 | */ 172 | private String getCreateViewStatement(String view) throws SQLException { 173 | 174 | StringBuilder sql = new StringBuilder(); 175 | ResultSet rs; 176 | 177 | if(view != null && !view.isEmpty()) { 178 | rs = stmt.executeQuery("SHOW CREATE VIEW " + "`" + view + "`;"); 179 | while ( rs.next() ) { 180 | String viewName = rs.getString(1); 181 | String viewQuery = rs.getString(2); 182 | sql.append("\n\n--"); 183 | sql.append("\n").append(MysqlBaseService.SQL_START_PATTERN).append(" view dump : ").append(view); 184 | sql.append("\n--\n\n"); 185 | 186 | String finalQuery = "CREATE OR REPLACE VIEW `" + viewName + "` " + (viewQuery.substring(viewQuery.indexOf("AS")).trim()); 187 | sql.append(finalQuery).append(";\n\n"); 188 | } 189 | 190 | sql.append("\n\n--"); 191 | sql.append("\n").append(MysqlBaseService.SQL_END_PATTERN).append(" view dump : ").append(view); 192 | sql.append("\n--\n\n"); 193 | } 194 | 195 | return sql.toString(); 196 | } 197 | 198 | 199 | /** 200 | * This function will generate the insert statements needed 201 | * to recreate the table under processing. 202 | * @param table the table to get inserts statement for 203 | * @return String generated SQL insert 204 | * @throws SQLException exception 205 | */ 206 | private String getDataInsertStatement(String table) throws SQLException { 207 | 208 | StringBuilder sql = new StringBuilder(); 209 | 210 | ResultSet rs = stmt.executeQuery("SELECT * FROM " + "`" + table + "`;"); 211 | 212 | //move to the last row to get max rows returned 213 | rs.last(); 214 | int rowCount = rs.getRow(); 215 | 216 | //there are no records just return empty string 217 | if(rowCount <= 0) { 218 | return sql.toString(); 219 | } 220 | 221 | sql.append("\n--").append("\n-- Inserts of ").append(table).append("\n--\n\n"); 222 | 223 | //temporarily disable foreign key constraint 224 | sql.append("\n/*!40000 ALTER TABLE `").append(table).append("` DISABLE KEYS */;\n"); 225 | 226 | sql.append("\n--\n") 227 | .append(MysqlBaseService.SQL_START_PATTERN).append(" table insert : ").append(table) 228 | .append("\n--\n"); 229 | 230 | sql.append("INSERT INTO `").append(table).append("`("); 231 | 232 | ResultSetMetaData metaData = rs.getMetaData(); 233 | int columnCount = metaData.getColumnCount(); 234 | 235 | //generate the column names that are present 236 | //in the returned result set 237 | //at this point the insert is INSERT INTO (`col1`, `col2`, ...) 238 | for(int i = 0; i < columnCount; i++) { 239 | sql.append("`") 240 | .append(metaData.getColumnName( i + 1)) 241 | .append("`, "); 242 | } 243 | 244 | //remove the last whitespace and comma 245 | sql.deleteCharAt(sql.length() - 1).deleteCharAt(sql.length() - 1).append(") VALUES \n"); 246 | 247 | //now we're going to build the values for data insertion 248 | rs.beforeFirst(); 249 | while(rs.next()) { 250 | sql.append("("); 251 | for(int i = 0; i < columnCount; i++) { 252 | 253 | int columnType = metaData.getColumnType(i + 1); 254 | int columnIndex = i + 1; 255 | 256 | //this is the part where the values are processed based on their type 257 | if(Objects.isNull(rs.getObject(columnIndex))) { 258 | sql.append(rs.getObject(columnIndex)).append(", "); 259 | } 260 | else if( columnType == Types.INTEGER || columnType == Types.TINYINT || columnType == Types.BIT 261 | || columnType == Types.SMALLINT || columnType == Types.BIGINT) { 262 | sql.append(rs.getInt(columnIndex)).append(", "); 263 | } 264 | else if(columnType == Types.REAL || columnType == Types.FLOAT || columnType == Types.DOUBLE || columnType == Types.DECIMAL 265 | || columnType == Types.NUMERIC) { 266 | sql.append(rs.getDouble(columnIndex)).append(", "); 267 | } 268 | 269 | else if(columnType == Types.BINARY || columnType == Types.BLOB || columnType == Types.LONGVARBINARY || columnType == Types.VARBINARY) { 270 | sql.append("0x").append(bytesToHex(rs.getBytes(columnIndex))).append(", "); 271 | } 272 | else { 273 | 274 | String val = rs.getString(columnIndex); 275 | //escape the single quotes that might be in the value 276 | val = val.replace("'", "\\'"); 277 | 278 | sql.append("'").append(val).append("', "); 279 | } 280 | } 281 | 282 | //now that we're done with a row 283 | //let's remove the last whitespace and comma 284 | sql.deleteCharAt(sql.length() - 1).deleteCharAt(sql.length() - 1); 285 | 286 | //if this is the last row, just append a closing 287 | //parenthesis otherwise append a closing parenthesis and a comma 288 | //for the next set of values 289 | if(rs.isLast()) { 290 | sql.append(")"); 291 | } else { 292 | sql.append("),\n"); 293 | } 294 | } 295 | 296 | //now that we are done processing the entire row 297 | //let's add the terminator 298 | sql.append(";"); 299 | 300 | sql.append("\n--\n") 301 | .append(MysqlBaseService.SQL_END_PATTERN).append(" table insert : ").append(table) 302 | .append("\n--\n"); 303 | 304 | //enable FK constraint 305 | sql.append("\n/*!40000 ALTER TABLE `").append(table).append("` ENABLE KEYS */;\n"); 306 | 307 | return sql.toString(); 308 | } 309 | 310 | 311 | /** 312 | * This is the entry function that'll 313 | * coordinate getTableInsertStatement() and getDataInsertStatement() 314 | * for every table in the database to generate a whole 315 | * script of SQL 316 | * @return String 317 | * @throws SQLException exception 318 | */ 319 | private String exportToSql() throws SQLException { 320 | 321 | StringBuilder sql = new StringBuilder(); 322 | sql.append("--"); 323 | sql.append("\n-- Generated by mysql-backup4j"); 324 | sql.append("\n-- https://github.com/SeunMatt/mysql-backup4j"); 325 | sql.append("\n-- Date: ").append(new SimpleDateFormat("d-M-Y H:m:s").format(new Date())); 326 | sql.append("\n--"); 327 | 328 | //these declarations are extracted from HeidiSQL 329 | sql.append("\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;") 330 | .append("\n/*!40101 SET NAMES utf8 */;") 331 | .append("\n/*!50503 SET NAMES utf8mb4 */;") 332 | .append("\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;") 333 | .append("\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;"); 334 | 335 | 336 | //get the tables that are in the database 337 | // List tables = MysqlBaseService.getAllTables(database, stmt); 338 | TablesResponse allTablesAndViews = MysqlBaseService.getAllTablesAndViews(database, stmt); 339 | 340 | List tables = allTablesAndViews.getTables(); 341 | //for every table, get the table creation and data 342 | // insert statement 343 | for (String s: tables) { 344 | try { 345 | sql.append(getTableInsertStatement(s.trim())); 346 | sql.append(getDataInsertStatement(s.trim())); 347 | } catch (SQLException e) { 348 | logger.error("Exception occurred while processing table: " + s, e); 349 | } 350 | } 351 | 352 | 353 | //process views if there's any 354 | List views = allTablesAndViews.getViews(); 355 | for (String v: views) { 356 | try { 357 | sql.append(getCreateViewStatement(v.trim())); 358 | } catch (SQLException e) { 359 | logger.error("Exception occurred while processing view: " + v, e); 360 | } 361 | } 362 | 363 | sql.append("\n/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;") 364 | .append("\n/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;") 365 | .append("\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;"); 366 | 367 | this.generatedSql = sql.toString(); 368 | return sql.toString(); 369 | } 370 | 371 | /** 372 | * This is the entry point for exporting 373 | * the database. It performs validation and 374 | * the initial object initializations, 375 | * database connection and setup 376 | * before ca 377 | * @throws IOException exception 378 | * @throws SQLException exception 379 | * @throws ClassNotFoundException exception 380 | */ 381 | public void export() throws IOException, SQLException, ClassNotFoundException { 382 | 383 | //check if properties is set or not 384 | if(!isValidateProperties()) { 385 | String message = "Invalid config properties: The config properties is missing important parameters: DB_NAME, DB_USERNAME and DB_PASSWORD"; 386 | logger.error(message); 387 | throw new MysqlBackup4JException(message); 388 | } 389 | 390 | //connect to the database 391 | database = properties.getProperty(DB_NAME); 392 | String jdbcURL = properties.getProperty(JDBC_CONNECTION_STRING, ""); 393 | String driverName = properties.getProperty(JDBC_DRIVER_NAME, ""); 394 | 395 | Connection connection; 396 | 397 | if(jdbcURL == null || jdbcURL.isEmpty()) { 398 | connection = MysqlBaseService.connect( 399 | properties.getProperty(DB_USERNAME), 400 | properties.getProperty(DB_PASSWORD), 401 | properties.getProperty(DB_HOST, "localhost"), 402 | properties.getProperty(DB_PORT, "3306"), 403 | database, 404 | driverName); 405 | } 406 | else { 407 | //this prioritizes the value set using the setDatabase() over the one extracted from the connection string 408 | //it will only use the one from the connection string if no value is set using the setDatabase() 409 | if(database == null || database.isEmpty()) { 410 | database = MysqlBaseService.extractDatabaseNameFromJDBCUrl(jdbcURL); 411 | } 412 | connection = MysqlBaseService.connectWithURL(properties.getProperty(DB_USERNAME), properties.getProperty(DB_PASSWORD), 413 | jdbcURL, driverName); 414 | } 415 | 416 | stmt = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); 417 | 418 | //generate the final SQL 419 | String sql = exportToSql(); 420 | 421 | //close the statement 422 | stmt.close(); 423 | 424 | //close the connection 425 | connection.close(); 426 | 427 | //create a temp dir to store the exported file for processing 428 | dirName = properties.getProperty(MysqlExportService.TEMP_DIR, dirName); 429 | File file = new File(dirName); 430 | if(!file.exists()) { 431 | boolean res = file.mkdir(); 432 | if(!res) { 433 | throw new IOException(LOG_PREFIX + ": Unable to create temp dir: " + file.getAbsolutePath()); 434 | } 435 | } 436 | 437 | //write the sql file out 438 | File sqlFolder = new File(dirName + "/sql"); 439 | if(!sqlFolder.exists()) { 440 | boolean res = sqlFolder.mkdir(); 441 | if(!res) { 442 | throw new IOException(LOG_PREFIX + ": Unable to create temp dir: " + file.getAbsolutePath()); 443 | } 444 | } 445 | 446 | sqlFileName = getSqlFilename(); 447 | FileOutputStream outputStream = new FileOutputStream( sqlFolder + "/" + sqlFileName); 448 | outputStream.write(sql.getBytes()); 449 | outputStream.close(); 450 | 451 | //zip the file 452 | zipFileName = dirName + "/" + sqlFileName.replace(".sql", ".zip"); 453 | generatedZipFile = new File(zipFileName); 454 | ZipUtil.pack(sqlFolder, generatedZipFile); 455 | 456 | //mail the zipped file if mail settings are available 457 | if(isEmailPropertiesSet()) { 458 | boolean emailSendingRes = EmailService.builder() 459 | .setHost(properties.getProperty(EMAIL_HOST)) 460 | .setPort(Integer.parseInt(properties.getProperty(EMAIL_PORT))) 461 | .setToAddress(properties.getProperty(EMAIL_TO)) 462 | .setFromAddress(properties.getProperty(EMAIL_FROM)) 463 | .setUsername(properties.getProperty(EMAIL_USERNAME)) 464 | .setPassword(properties.getProperty(EMAIL_PASSWORD)) 465 | .setSslProtocols(properties.getProperty(EMAIL_SSL_PROTOCOLS, "TLSv1.2")) 466 | .setStartTlsEnabled(properties.getProperty(EMAIL_START_TLS_ENABLED, "true")) 467 | .setSmtpAuthEnabled(Boolean.parseBoolean(properties.getProperty(EMAIL_SMTP_AUTH_ENABLED, "true"))) 468 | .setSubject(properties.getProperty(EMAIL_SUBJECT, sqlFileName.replace(".sql", "").toUpperCase())) 469 | .setMessage(properties.getProperty(EMAIL_MESSAGE, "Please find attached database backup of " + database)) 470 | .setAttachments(new File[]{new File(zipFileName)}) 471 | .sendMail(); 472 | 473 | if (emailSendingRes) { 474 | logger.debug(LOG_PREFIX + ": Zip File Sent as Attachment to Email Address Successfully"); 475 | } else { 476 | logger.error(LOG_PREFIX + ": Unable to send zipped file as attachment to email. See log debug for more info"); 477 | } 478 | } 479 | 480 | //clear the generated temp files 481 | clearTempFiles(); 482 | 483 | } 484 | 485 | /** 486 | * This function will delete all the 487 | * temp files generated ny the library 488 | * unless it's otherwise instructed not to do 489 | * so by the preserveZipFile variable 490 | * 491 | */ 492 | public void clearTempFiles() { 493 | 494 | 495 | if(!Boolean.parseBoolean(properties.getProperty(PRESERVE_GENERATED_SQL_FILE, Boolean.FALSE.toString()))) { 496 | //delete the temp sql file 497 | File sqlFile = new File(dirName + "/sql/" + sqlFileName); 498 | if (sqlFile.exists()) { 499 | boolean res = sqlFile.delete(); 500 | logger.debug(LOG_PREFIX + ": " + sqlFile.getAbsolutePath() + " deleted successfully? " + (res ? " TRUE " : " FALSE ")); 501 | } else { 502 | logger.debug(LOG_PREFIX + ": " + sqlFile.getAbsolutePath() + " DOES NOT EXIST while clearing Temp Files"); 503 | } 504 | 505 | File sqlFolder = new File(dirName + "/sql"); 506 | if (sqlFolder.exists()) { 507 | boolean res = sqlFolder.delete(); 508 | logger.debug(LOG_PREFIX + ": " + sqlFolder.getAbsolutePath() + " deleted successfully? " + (res ? " TRUE " : " FALSE ")); 509 | } else { 510 | logger.debug(LOG_PREFIX + ": " + sqlFolder.getAbsolutePath() + " DOES NOT EXIST while clearing Temp Files"); 511 | } 512 | 513 | } 514 | 515 | 516 | //only execute this section if the 517 | //file is not to be preserved 518 | if(!Boolean.parseBoolean(properties.getProperty(PRESERVE_GENERATED_ZIP, Boolean.FALSE.toString()))) { 519 | 520 | //delete the zipFile 521 | File zipFile = new File(zipFileName); 522 | if (zipFile.exists()) { 523 | boolean res = zipFile.delete(); 524 | logger.debug(LOG_PREFIX + ": " + zipFile.getAbsolutePath() + " deleted successfully? " + (res ? " TRUE " : " FALSE ")); 525 | } else { 526 | logger.debug(LOG_PREFIX + ": " + zipFile.getAbsolutePath() + " DOES NOT EXIST while clearing Temp Files"); 527 | } 528 | 529 | //delete the temp folder 530 | File folder = new File(dirName); 531 | if (folder.exists()) { 532 | boolean res = folder.delete(); 533 | logger.debug(LOG_PREFIX + ": " + folder.getAbsolutePath() + " deleted successfully? " + (res ? " TRUE " : " FALSE ")); 534 | } else { 535 | logger.debug(LOG_PREFIX + ": " + folder.getAbsolutePath() + " DOES NOT EXIST while clearing Temp Files"); 536 | } 537 | } 538 | 539 | logger.debug(LOG_PREFIX + ": generated temp files cleared successfully"); 540 | } 541 | 542 | /** 543 | * This will get the final output 544 | * sql file name. 545 | * @return String 546 | */ 547 | public String getSqlFilename(){ 548 | return isSqlFileNamePropertySet() ? properties.getProperty(SQL_FILE_NAME) + ".sql" : 549 | new SimpleDateFormat("d_M_Y_H_mm_ss").format(new Date()) + "_" + database + "_database_dump.sql"; 550 | } 551 | 552 | public String getSqlFileName() { 553 | return sqlFileName; 554 | } 555 | 556 | /** 557 | * this is a getter for the raw sql generated in the backup process 558 | * @return generatedSql 559 | */ 560 | public String getGeneratedSql() { 561 | return generatedSql; 562 | } 563 | 564 | /** 565 | * this is a getter for the generatedZipFile generatedZipFile File object 566 | * The reference can be used for further processing in 567 | * external systems 568 | * @return generatedZipFile or null 569 | */ 570 | public File getGeneratedZipFile() { 571 | if(generatedZipFile != null && generatedZipFile.exists()) { 572 | return generatedZipFile; 573 | } 574 | return null; 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/MysqlImportService.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import com.smattme.exceptions.MysqlBackup4JException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.sql.Connection; 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | /** 15 | * Created by seun_ on 01-Mar-18. 16 | * 17 | */ 18 | public class MysqlImportService { 19 | 20 | private String database; 21 | private String username; 22 | private String host = "localhost"; 23 | private String port = "3306"; 24 | private String password; 25 | private String sqlString; 26 | private String jdbcConnString; 27 | private String jdbcDriver; 28 | private boolean deleteExisting; 29 | private boolean dropExisting; 30 | private List tables; 31 | private Logger logger = LoggerFactory.getLogger(MysqlImportService.class); 32 | 33 | private MysqlImportService() { 34 | this.deleteExisting = false; 35 | this.dropExisting = false; 36 | this.tables = new ArrayList<>(); 37 | } 38 | 39 | /** 40 | * 41 | * @return bool 42 | * @throws SQLException exception 43 | * @throws ClassNotFoundException exception 44 | */ 45 | public boolean importDatabase() throws SQLException, ClassNotFoundException { 46 | 47 | if(!this.assertValidParams()) { 48 | String message = "Required Parameters not set or empty \n" + 49 | "Ensure database, username, password, sqlString params are configured \n" + 50 | "using their respective setters"; 51 | logger.error(message); 52 | throw new MysqlBackup4JException(message); 53 | } 54 | 55 | 56 | //connect to the database 57 | Connection connection; 58 | if(jdbcConnString == null || jdbcConnString.isEmpty()) { 59 | connection = MysqlBaseService.connect(username, password, host, port, database, jdbcDriver); 60 | } 61 | else { 62 | 63 | //this prioritizes the value set using the setDatabase() over the one extracted from the connection string 64 | //it will only use the one from the connection string if no value is set using the setDatabase() 65 | if(database == null || database.isEmpty()) { 66 | database = MysqlBaseService.extractDatabaseNameFromJDBCUrl(jdbcConnString); 67 | logger.debug("database name extracted from connection string: " + database); 68 | } 69 | 70 | connection = MysqlBaseService.connectWithURL(username, password, jdbcConnString, jdbcDriver); 71 | } 72 | 73 | Statement stmt = connection.createStatement(); 74 | 75 | if(deleteExisting || dropExisting) { 76 | 77 | //get all the tables, so as to eliminate delete errors due to non-existent tables 78 | TablesResponse allTablesAndViews = MysqlBaseService.getAllTablesAndViews(database, stmt); 79 | tables = allTablesAndViews.getTables(); 80 | logger.debug("tables found for deleting/dropping: \n" + tables.toString()); 81 | 82 | 83 | //execute delete query for tables 84 | for (String table: tables) { 85 | 86 | //if deleteExisting and dropExisting is true 87 | //skip the deleteExisting query 88 | //dropExisting will take care of both 89 | if(deleteExisting && !dropExisting) { 90 | String delQ = "DELETE FROM " + "`" + table + "`;"; 91 | logger.debug("adding " + delQ + " to batch"); 92 | stmt.addBatch(delQ); 93 | } 94 | 95 | if(dropExisting) { 96 | String dropQ = "DROP TABLE IF EXISTS " + "`" + table + "`"; 97 | logger.debug("adding " + dropQ + " to batch"); 98 | stmt.addBatch(dropQ); 99 | } 100 | 101 | } 102 | 103 | 104 | List views = allTablesAndViews.getViews(); 105 | //execute delete query for views 106 | for (String view: views) { 107 | if(dropExisting) { 108 | String dropQ = "DROP VIEW IF EXISTS " + "`" + view + "`"; 109 | logger.debug("adding " + dropQ + " to batch"); 110 | stmt.addBatch(dropQ); 111 | } 112 | } 113 | 114 | } 115 | 116 | //disable foreign key check 117 | stmt.addBatch("SET FOREIGN_KEY_CHECKS = 0"); 118 | 119 | 120 | //now process the sql string supplied 121 | while (sqlString.contains(MysqlBaseService.SQL_START_PATTERN)) { 122 | 123 | //get the chunk of the first statement to execute 124 | int startIndex = sqlString.indexOf(MysqlBaseService.SQL_START_PATTERN); 125 | int endIndex = sqlString.indexOf(MysqlBaseService.SQL_END_PATTERN); 126 | 127 | String executable = sqlString.substring(startIndex, endIndex).trim(); 128 | logger.debug("adding extracted executable SQL chunk to batch : \n" + executable); 129 | stmt.addBatch(executable); 130 | 131 | //remove the chunk from the whole to reduce it 132 | sqlString = sqlString.substring(endIndex + 1); 133 | 134 | //repeat 135 | } 136 | 137 | 138 | //add enable foreign key check 139 | stmt.addBatch("SET FOREIGN_KEY_CHECKS = 1"); 140 | 141 | //now execute the batch 142 | long[] result = stmt.executeLargeBatch(); 143 | 144 | if(logger.isDebugEnabled()) 145 | logger.debug( result.length + " queries were executed in batches for provided SQL String with the following result : \n" + Arrays.toString(result)); 146 | 147 | stmt.close(); 148 | connection.close(); 149 | 150 | return true; 151 | } 152 | 153 | /** 154 | * This function will check that required parameters 155 | * are set. 156 | * password is excluded here because it's possible to have a mysql database 157 | * user with no password 158 | * @return true if the required params are present and valid, false otherwise 159 | */ 160 | private boolean assertValidParams() { 161 | return username != null && !this.username.isEmpty() && 162 | sqlString != null && !this.sqlString.isEmpty() && 163 | ( (database != null && !this.database.isEmpty()) || (jdbcConnString != null && !jdbcConnString.isEmpty()) ); 164 | } 165 | 166 | /** 167 | * This function will create a new 168 | * MysqlImportService instance thereby facilitating 169 | * a builder pattern 170 | * @return MysqlImportService 171 | */ 172 | public static MysqlImportService builder() { 173 | return new MysqlImportService(); 174 | } 175 | 176 | public MysqlImportService setDatabase(String database) { 177 | this.database = database; 178 | return this; 179 | } 180 | 181 | public MysqlImportService setUsername(String username) { 182 | this.username = username; 183 | return this; 184 | } 185 | 186 | public MysqlImportService setPassword(String password) { 187 | this.password = password; 188 | return this; 189 | } 190 | 191 | public MysqlImportService setSqlString(String sqlString) { 192 | this.sqlString = sqlString; 193 | return this; 194 | } 195 | 196 | public MysqlImportService setDeleteExisting(boolean deleteExisting) { 197 | this.deleteExisting = deleteExisting; 198 | return this; 199 | } 200 | 201 | public MysqlImportService setDropExisting(boolean dropExistingTable) { 202 | this.dropExisting = dropExistingTable; 203 | return this; 204 | } 205 | 206 | public MysqlImportService setJdbcDriver(String jdbcDriver) { 207 | this.jdbcDriver = jdbcDriver; 208 | return this; 209 | } 210 | 211 | public MysqlImportService setJdbcConnString(String jdbcConnString) { 212 | this.jdbcConnString = jdbcConnString; 213 | return this; 214 | } 215 | 216 | public MysqlImportService setHost(String host) { 217 | this.host = host; 218 | return this; 219 | } 220 | 221 | public MysqlImportService setPort(String port) { 222 | this.port = port; 223 | return this; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/TablesResponse.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import java.util.List; 4 | 5 | public class TablesResponse { 6 | 7 | private List tables; 8 | private List views; 9 | 10 | public TablesResponse() { } 11 | 12 | public TablesResponse(List tables, List views) { 13 | this.tables = tables; 14 | this.views = views; 15 | } 16 | 17 | public List getTables() { 18 | return tables; 19 | } 20 | 21 | public List getViews() { 22 | return views; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/exceptions/MysqlBackup4JException.java: -------------------------------------------------------------------------------- 1 | package com.smattme.exceptions; 2 | 3 | public class MysqlBackup4JException extends RuntimeException { 4 | 5 | public MysqlBackup4JException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/smattme/helpers/MysqlExportServiceHelper.java: -------------------------------------------------------------------------------- 1 | package com.smattme.helpers; 2 | 3 | public class MysqlExportServiceHelper { 4 | 5 | public static String bytesToHex(byte[] bytes) { 6 | StringBuilder hexString = new StringBuilder(); 7 | for (byte b : bytes) { 8 | String hex = Integer.toHexString(0xff & b); 9 | if (hex.length() == 1) hexString.append('0'); 10 | hexString.append(hex); 11 | } 12 | return hexString.toString(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/smattme/MysqlBackup4JIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import org.junit.jupiter.api.AfterAll; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.testcontainers.containers.GenericContainer; 10 | import org.testcontainers.containers.MySQLContainer; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.File; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.net.URL; 17 | import java.nio.file.Files; 18 | import java.sql.Connection; 19 | import java.sql.ResultSet; 20 | import java.sql.Statement; 21 | import java.util.Objects; 22 | import java.util.Properties; 23 | import java.util.regex.Matcher; 24 | import java.util.regex.Pattern; 25 | import java.util.stream.Collectors; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertNotNull; 28 | import static org.junit.jupiter.api.Assertions.assertTrue; 29 | 30 | /** 31 | * Created by seun_ on 10-Oct-20. 32 | * 33 | */ 34 | class MysqlBackup4JIntegrationTest { 35 | 36 | private static final Logger logger = LoggerFactory.getLogger(MysqlBackup4JIntegrationTest.class); 37 | private static final String TEST_DB = "mysqlbackup4j_test"; 38 | private static final String RESTORED_DB = "mysqlbackup4j_restored"; 39 | private static final String DB_USERNAME = "root"; 40 | private static final String DB_PASSWORD = "backup4j"; 41 | private static final String DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver"; 42 | protected static String MYSQL_DB_PORT = "3306"; 43 | protected static String MYSQL_DB_HOST = "localhost"; 44 | 45 | private static final MySQLContainer mysql = new MySQLContainer<>("mysql:8.1.0"); 46 | 47 | @BeforeAll 48 | static void setUp() { 49 | mysql.withUsername(DB_USERNAME) 50 | .withPassword(DB_PASSWORD) 51 | .withExposedPorts(3306) 52 | .withInitScript("mysql_init.sql") 53 | .start(); 54 | MYSQL_DB_PORT = mysql.getMappedPort(3306).toString(); 55 | MYSQL_DB_HOST = mysql.getHost(); 56 | logger.info("MYSQL_DB_HOST: {}, MYSQL_DB_PORT: {}", MYSQL_DB_HOST, MYSQL_DB_PORT); 57 | } 58 | 59 | @AfterAll 60 | static void tearDown() { 61 | if (Objects.nonNull(mysql)) { 62 | mysql.stop(); 63 | } 64 | } 65 | 66 | 67 | @Test 68 | void givenDBCredentials_whenExportDatabaseAndImportDatabase_thenBackUpAndRestoreTestDbSuccessfully() throws Exception { 69 | 70 | Properties properties = new Properties(); 71 | properties.setProperty(MysqlExportService.DB_NAME, TEST_DB); 72 | properties.setProperty(MysqlExportService.DB_USERNAME, DB_USERNAME); 73 | properties.setProperty(MysqlExportService.DB_PASSWORD, DB_PASSWORD); 74 | 75 | properties.setProperty(MysqlExportService.DB_HOST, MYSQL_DB_HOST); 76 | properties.setProperty(MysqlExportService.DB_PORT, MYSQL_DB_PORT); 77 | 78 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_ZIP, "true"); 79 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_SQL_FILE, "true"); 80 | 81 | properties.setProperty(MysqlExportService.JDBC_DRIVER_NAME, DRIVER_CLASS_NAME); 82 | properties.setProperty(MysqlExportService.ADD_IF_NOT_EXISTS, "true"); 83 | 84 | 85 | properties.setProperty(MysqlExportService.TEMP_DIR, new File("external").getPath()); 86 | properties.setProperty(MysqlExportService.SQL_FILE_NAME, "test_output_file_name"); 87 | 88 | MysqlExportService mysqlExportService = new MysqlExportService(properties); 89 | mysqlExportService.export(); 90 | 91 | String generatedSql = mysqlExportService.getGeneratedSql(); 92 | Assertions.assertFalse(generatedSql.isEmpty()); 93 | // logger.info("generated SQL: \n" + generatedSql); 94 | 95 | File file = mysqlExportService.getGeneratedZipFile(); 96 | assertNotNull(file); 97 | logger.info("Generated Filename: " + file.getAbsolutePath()); 98 | 99 | File sqlFile = new File("external/sql/test_output_file_name.sql"); 100 | logger.info("SQL File name: " + sqlFile.getAbsolutePath()); 101 | 102 | String sql = new String(Files.readAllBytes(sqlFile.toPath())); 103 | MysqlImportService res = MysqlImportService.builder() 104 | .setJdbcDriver("com.mysql.cj.jdbc.Driver") 105 | .setDatabase(RESTORED_DB) 106 | .setSqlString(sql) 107 | .setUsername(DB_USERNAME) 108 | .setPassword(DB_PASSWORD) 109 | .setHost(MYSQL_DB_HOST) 110 | .setPort(MYSQL_DB_PORT) 111 | .setDeleteExisting(true) 112 | .setDropExisting(true); 113 | 114 | assertTrue(res.importDatabase()); 115 | 116 | assertDatabaseBackedUp(); 117 | 118 | } 119 | 120 | 121 | @Test 122 | void givenDBCredentialsAndEmailConfig_whenExportDatabase_thenBackUpAndMailDbSuccessfully() throws Exception { 123 | 124 | GenericContainer smtpServerContainer = 125 | new GenericContainer<>("reachfive/fake-smtp-server:0.8.1") 126 | .withExposedPorts(1080, 1025); 127 | smtpServerContainer.start(); 128 | int smtpPort = smtpServerContainer.getMappedPort(1025); 129 | int webPort = smtpServerContainer.getMappedPort(1080); 130 | String smtpServerHost = smtpServerContainer.getHost(); 131 | 132 | 133 | Properties properties = new Properties(); 134 | properties.setProperty(MysqlExportService.DB_NAME, TEST_DB); 135 | properties.setProperty(MysqlExportService.DB_USERNAME, DB_USERNAME); 136 | properties.setProperty(MysqlExportService.DB_PASSWORD, DB_PASSWORD); 137 | 138 | properties.setProperty(MysqlExportService.DB_HOST, MYSQL_DB_HOST); 139 | properties.setProperty(MysqlExportService.DB_PORT, MYSQL_DB_PORT); 140 | 141 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_ZIP, "true"); 142 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_SQL_FILE, "true"); 143 | 144 | properties.setProperty(MysqlExportService.JDBC_DRIVER_NAME, DRIVER_CLASS_NAME); 145 | properties.setProperty(MysqlExportService.ADD_IF_NOT_EXISTS, "true"); 146 | 147 | properties.setProperty(MysqlExportService.TEMP_DIR, new File("external").getPath()); 148 | properties.setProperty(MysqlExportService.SQL_FILE_NAME, "test_output_file_name"); 149 | 150 | //properties relating to email config 151 | properties.setProperty(MysqlExportService.EMAIL_HOST, smtpServerHost); 152 | properties.setProperty(MysqlExportService.EMAIL_PORT, String.valueOf(smtpPort)); 153 | properties.setProperty(MysqlExportService.EMAIL_USERNAME, "username"); 154 | properties.setProperty(MysqlExportService.EMAIL_PASSWORD, "password"); 155 | properties.setProperty(MysqlExportService.EMAIL_FROM, "test@smattme.com"); 156 | properties.setProperty(MysqlExportService.EMAIL_TO, "backup@smattme.com"); 157 | properties.setProperty(MysqlExportService.EMAIL_SSL_PROTOCOLS, "TLSv1.2"); 158 | properties.setProperty(MysqlExportService.EMAIL_SMTP_AUTH_ENABLED, "true"); 159 | properties.setProperty(MysqlExportService.EMAIL_START_TLS_ENABLED, "true"); 160 | 161 | MysqlExportService mysqlExportService = new MysqlExportService(properties); 162 | mysqlExportService.export(); 163 | 164 | 165 | String url = String.format("http://%s:%s/api/emails?from=test@smattme.com&to=backup@smattme.com", 166 | smtpServerHost, webPort); 167 | 168 | InputStream inputStream = new URL(url).openConnection().getInputStream(); 169 | try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { 170 | String response = bufferedReader.lines().collect(Collectors.joining()); 171 | Pattern pattern = Pattern.compile("(.)*\"messageId\":(?\".+\").*"); 172 | Matcher matcher = pattern.matcher(response); 173 | Assertions.assertEquals(2, matcher.groupCount()); 174 | Assertions.assertTrue(matcher.matches()); 175 | Assertions.assertNotNull(matcher.group("msgId")); 176 | } 177 | 178 | 179 | inputStream.close(); 180 | smtpServerContainer.stop(); 181 | 182 | 183 | } 184 | 185 | 186 | @Test 187 | void givenJDBCConString_whenExportDatabaseAndImportDatabase_thenBackUpAndRestoreTestDbSuccessfully() throws Exception { 188 | 189 | Properties properties = new Properties(); 190 | properties.setProperty(MysqlExportService.DB_USERNAME, DB_USERNAME); 191 | properties.setProperty(MysqlExportService.DB_PASSWORD, DB_PASSWORD); 192 | properties.setProperty(MysqlExportService.DB_NAME, TEST_DB); 193 | String jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false", 194 | MYSQL_DB_HOST, MYSQL_DB_PORT, TEST_DB); 195 | properties.setProperty(MysqlExportService.JDBC_CONNECTION_STRING, jdbcUrl); 196 | 197 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_ZIP, "true"); 198 | properties.setProperty(MysqlExportService.PRESERVE_GENERATED_SQL_FILE, "true"); 199 | properties.setProperty(MysqlExportService.SQL_FILE_NAME, "test_output_file_name"); 200 | properties.setProperty(MysqlExportService.ADD_IF_NOT_EXISTS, "true"); 201 | 202 | properties.setProperty(MysqlExportService.TEMP_DIR, new File("external").getPath()); 203 | 204 | MysqlExportService mysqlExportService = new MysqlExportService(properties); 205 | mysqlExportService.export(); 206 | 207 | String generatedSql = mysqlExportService.getGeneratedSql(); 208 | // logger.debug("Final Output:\n {}", generatedSql); 209 | 210 | File file = mysqlExportService.getGeneratedZipFile(); 211 | assertNotNull(file); 212 | Assertions.assertEquals("test_output_file_name.zip", file.getName()); 213 | 214 | 215 | //import 216 | File sqlFile = new File("external/sql/test_output_file_name.sql"); 217 | 218 | String sql = new String(Files.readAllBytes(sqlFile.toPath())); 219 | String restoredJdbcUrl = String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false" 220 | + "&serverTimezone=UTC&useSSL=false", 221 | MYSQL_DB_HOST, MYSQL_DB_PORT, RESTORED_DB); 222 | boolean res = MysqlImportService.builder() 223 | .setSqlString(sql) 224 | .setJdbcConnString(restoredJdbcUrl) 225 | .setUsername(DB_USERNAME) 226 | .setPassword(DB_PASSWORD) 227 | .setDatabase(RESTORED_DB) 228 | .setDeleteExisting(true) 229 | .setDropExisting(true) 230 | .importDatabase(); 231 | 232 | assertTrue(res); 233 | 234 | assertDatabaseBackedUp(); 235 | } 236 | 237 | 238 | private void assertDatabaseBackedUp() throws Exception { 239 | Connection connection = MysqlBaseService.connect(DB_USERNAME, DB_PASSWORD, MYSQL_DB_HOST, MYSQL_DB_PORT, 240 | RESTORED_DB, DRIVER_CLASS_NAME); 241 | Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); 242 | statement.execute("SELECT COUNT(1) as total FROM users"); 243 | ResultSet resultSet = statement.getResultSet(); 244 | resultSet.first(); 245 | assertTrue(resultSet.getLong("total") > 0); 246 | } 247 | 248 | } -------------------------------------------------------------------------------- /src/test/java/com/smattme/MysqlBackup4JUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.smattme; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class MysqlBackup4JUnitTest { 7 | 8 | 9 | @Test 10 | void givenJDBCURL_whenExtractDatabaseNameFromJDBCURL_thenReturnDatabaseName() { 11 | String jdbcURL = "jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai"; 12 | String databaseName = MysqlBaseService.extractDatabaseNameFromJDBCUrl(jdbcURL); 13 | Assertions.assertEquals("test", databaseName); 14 | 15 | jdbcURL = "jdbc:mysql://localhost:3306/backup4j_test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false"; 16 | databaseName = MysqlBaseService.extractDatabaseNameFromJDBCUrl(jdbcURL); 17 | Assertions.assertEquals("backup4j_test", databaseName); 18 | 19 | jdbcURL = "jdbc:mysql://localhost:3306/backup4j_test"; 20 | databaseName = MysqlBaseService.extractDatabaseNameFromJDBCUrl(jdbcURL); 21 | Assertions.assertEquals("backup4j_test", databaseName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.log.org.testcontainers=info 2 | org.slf4j.simpleLogger.log.tc.mysql=info 3 | org.slf4j.simpleLogger.log.com.github.dockerjava=warn 4 | org.slf4j.simpleLogger.log.com.smattme=info 5 | org.slf4j.simpleLogger.defaultLogLevel=debug 6 | --------------------------------------------------------------------------------