diff --git a/CHANGELOG.md b/CHANGELOG.md
index c997b37726..4c97dd0025 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add markdown codeblock renderer extension point ([#1492](https://github.com/scm-manager/scm-manager/pull/1492))
- Add Java version to plugin center url ([#1494](https://github.com/scm-manager/scm-manager/pull/1494))
- Add Font ttf-dejavu to oci image ([#1498](https://github.com/scm-manager/scm-manager/issues/1498))
+- Add repository import and export with metadata for Subversion ([#1501](https://github.com/scm-manager/scm-manager/pull/1501))
+- API for store rename/delete in update steps ([#1505](https://github.com/scm-manager/scm-manager/pull/1505))
+
+### Changed
+- Directory name for git LFS files ([#1504](https://github.com/scm-manager/scm-manager/pull/1504))
### Changed
- Migrate integration tests to bdd ([#1497](https://github.com/scm-manager/scm-manager/pull/1497))
diff --git a/docs/de/user/repo/assets/import-repository-with-metadata.png b/docs/de/user/repo/assets/import-repository-with-metadata.png
new file mode 100644
index 0000000000..43b60b92c8
Binary files /dev/null and b/docs/de/user/repo/assets/import-repository-with-metadata.png differ
diff --git a/docs/de/user/repo/assets/repository-settings-general-svn.png b/docs/de/user/repo/assets/repository-settings-general-svn.png
index 8428eeaa25..d1a7184bc9 100644
Binary files a/docs/de/user/repo/assets/repository-settings-general-svn.png and b/docs/de/user/repo/assets/repository-settings-general-svn.png differ
diff --git a/docs/de/user/repo/index.md b/docs/de/user/repo/index.md
index aea842f5a7..0f6ae32107 100644
--- a/docs/de/user/repo/index.md
+++ b/docs/de/user/repo/index.md
@@ -47,8 +47,15 @@ Das gewählte Repository wird zum SCM-Manager hinzugefügt und sämtliche Reposi

+Für Subversion Repositories besteht die Möglichkeit, ein Repository inkl. Metadaten zu importieren.
+Dabei muss als Quelle ein Repository Archiv ausgewählt werden, welches vorher von einem SCM-Manager exportiert wurde.
+Der Import mit Metadaten unterstützt noch keine Migration der Plugin Daten,
+deshalb müssen die Versionen des SCM-Managers und die Versionen sämtlicher Plugins zwischen der exportierenden Instanz und der importierenden Instanz exakt übereinstimmen.
+Wenn sich die installierten Plugins zwischen diesen beiden Instanzen unterscheiden, sollte dies kein Problem verursachen.
+
### Repository Informationen
-Die Informationsseite eines Repository zeigt die Metadaten zum Repository an. Darunter befinden sich Beschreibungen zu den unterschiedlichen Möglichkeiten wie man mit diesem Repository arbeiten kann. In der Überschrift kann der Namespace angeklickt werden, um alle Repositories aus diesem Namespace anzuzeigen.
+Die Informationsseite eines Repository zeigt die Metadaten zum Repository an. Darunter befinden sich Beschreibungen zu den unterschiedlichen Möglichkeiten wie man mit diesem Repository arbeiten kann.
+In der Überschrift kann der Namespace angeklickt werden, um alle Repositories aus diesem Namespace anzuzeigen.

diff --git a/docs/de/user/repo/settings.md b/docs/de/user/repo/settings.md
index 7ecd807097..6771f39743 100644
--- a/docs/de/user/repo/settings.md
+++ b/docs/de/user/repo/settings.md
@@ -19,8 +19,9 @@ Ein archiviertes Repository kann nicht mehr verändert werden.

-In dem Bereich "Repository exportieren" kann das Repository als Dump exportiert werden.
-Für den Download kann zwischen einem komprimierten Dump oder dem einfachen Dump-Format gewählt werden.
+In dem Bereich "Repository exportieren" kann das Repository exportiert werden.
+Für den Download kann zwischen einem einfachen Dump des reinen Repositories und einem Repository Archiv inkl. der SCM-Manager Metadaten wie Plugin-Konfigurationen oder anderen Daten gewählt werden.
+Der Dump kann optional komprimiert werden. Das Repository Archiv mit Metadaten wird immer komprimiert ausgeliefert.
Diese Export-Funktion wird derzeit nur von Subversion Repositories unterstützt.

diff --git a/docs/en/user/repo/assets/import-repository-with-metadata.png b/docs/en/user/repo/assets/import-repository-with-metadata.png
new file mode 100644
index 0000000000..2c859aca9a
Binary files /dev/null and b/docs/en/user/repo/assets/import-repository-with-metadata.png differ
diff --git a/docs/en/user/repo/assets/repository-settings-general-svn.png b/docs/en/user/repo/assets/repository-settings-general-svn.png
index cb9b59ba70..e6e24a091e 100644
Binary files a/docs/en/user/repo/assets/repository-settings-general-svn.png and b/docs/en/user/repo/assets/repository-settings-general-svn.png differ
diff --git a/docs/en/user/repo/index.md b/docs/en/user/repo/index.md
index c078777634..6dce67aa9b 100644
--- a/docs/en/user/repo/index.md
+++ b/docs/en/user/repo/index.md
@@ -45,7 +45,18 @@ Your repository will be added to SCM-Manager and all repository data including a

+
+Subversion also supports the import of a repository archive with metadata.
+This repository archive must be exported from an SCM-Manager.
+This import mode doesn't support data migration yet.
+So the repository archive can only be imported if the version between the exporting SCM-Manager and the importing SCM-Manager and **also all plugin versions** are equal.
+If the installed plugins differ between those two instances it shouldn't create an issue.
+
+
+
### Repository Information
-The information screen of repositories shows meta data about the repository. Amongst that are descriptions for the different options on how the repository can be used. In the heading you can click the namespace to get the list of all repositories for this namespace.
+The information screen of repositories shows meta data about the repository.
+Amongst that are descriptions for the different options on how the repository can be used.
+In the heading you can click the namespace to get the list of all repositories for this namespace.

diff --git a/docs/en/user/repo/settings.md b/docs/en/user/repo/settings.md
index 7c1596e7a9..fc9d8eaed4 100644
--- a/docs/en/user/repo/settings.md
+++ b/docs/en/user/repo/settings.md
@@ -17,8 +17,9 @@ repository is marked as archived, it can no longer be modified.

-In the area "Repository Export" you may export this repository as dump file.
-You can choose between compressed and uncompressed download format.
+In the area "Repository Export" you may export this repository.
+You can choose to export a simple dump of the repository or a repository archive including all SCM-Manager metadata like plugin configuration or other data.
+For a simple repository dump you can choose between compressed or uncompressed file format. The repository archive is always compressed.
This export function is currently only supported by Subversion repositories.

diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java
index a5c784cd64..79ad1c6d19 100644
--- a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java
+++ b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.migration;
import sonia.scm.plugin.ExtensionPoint;
@@ -50,6 +50,7 @@ import sonia.scm.version.Version;
*
*
a {@link sonia.scm.security.KeyGenerator},
*
the {@link sonia.scm.repository.RepositoryLocationResolver},
+ *
an {@link sonia.scm.update.RepositoryUpdateIterator},
*
the {@link sonia.scm.io.FileSystem},
*
the {@link sonia.scm.security.CipherHandler},
*
a {@link sonia.scm.store.ConfigurationStoreFactory},
Mind that an implementation of this class has to be annotated with {@link sonia.scm.plugin.Extension}, so that the
+ * step will be found.
*/
@ExtensionPoint
public interface UpdateStep {
diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java
index c0e47713bd..d829d7f4fd 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Repository.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java
@@ -56,7 +56,7 @@ import java.util.Set;
@XmlRootElement(name = "repositories")
@StaticPermissions(
value = "repository",
- permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite", "archive"},
+ permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite", "archive", "export"},
custom = true, customGlobal = true,
guards = {
@Guard(guard = RepositoryPermissionGuard.class)
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ExportFailedException.java b/scm-core/src/main/java/sonia/scm/repository/api/ExportFailedException.java
new file mode 100644
index 0000000000..48db71f220
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ExportFailedException.java
@@ -0,0 +1,48 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository.api;
+
+import sonia.scm.ContextEntry;
+import sonia.scm.ExceptionWithContext;
+
+import java.util.List;
+
+public class ExportFailedException extends ExceptionWithContext {
+
+ private static final String CODE = "67SM3DANZ1";
+
+ public ExportFailedException(List context, String message, Exception cause) {
+ super(context, message, cause);
+ }
+
+ public ExportFailedException(List context, String message) {
+ super(context, message);
+ }
+
+ @Override
+ public String getCode() {
+ return CODE;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java b/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java
index 1d73104944..c79ed181ac 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java
@@ -42,6 +42,10 @@ public class ImportFailedException extends ExceptionWithContext {
super(context, message, cause);
}
+ public ImportFailedException(List context, String message) {
+ super(context, message);
+ }
+
@Override
public String getCode() {
return CODE;
diff --git a/scm-core/src/main/java/sonia/scm/store/ExportableStore.java b/scm-core/src/main/java/sonia/scm/store/ExportableStore.java
new file mode 100644
index 0000000000..322377d7c8
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/ExportableStore.java
@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.Beta;
+
+import java.io.IOException;
+
+/**
+ * The {@link ExportableStore} is used to export the stored data inside the store.
+ *
This interface is not yet finalized and might change in the upcoming versions.
+ *
+ * @since 2.13.0
+ */
+@Beta
+public interface ExportableStore {
+
+ /**
+ * Contains the information about this store.
+ */
+ StoreEntryMetaData getMetaData();
+
+ /**
+ * Exports the data of this store to the given {@param exporter}.
+ */
+ void export(Exporter exporter) throws IOException;
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/Exporter.java b/scm-core/src/main/java/sonia/scm/store/Exporter.java
new file mode 100644
index 0000000000..86a7355809
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/Exporter.java
@@ -0,0 +1,48 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.Beta;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The {@link Exporter} is used to export a single store entry to an {@link OutputStream}.
+ *
This interface is not yet finalized and might change in the upcoming versions.
+ *
+ * @since 2.13.0
+ */
+@Beta
+public interface Exporter {
+ /**
+ * Returns the {@link OutputStream} that should be used to export a single store entry with the given name.
+ *
+ * @param name The name of the exported store entry.
+ * @param size The size of the exported store entry (the size of the bytes that will be written to the output stream).
+ * @return The output stream the raw data of the store entry must be written to.
+ */
+ OutputStream put(String name, long size) throws IOException;
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreEntryImporter.java b/scm-core/src/main/java/sonia/scm/store/StoreEntryImporter.java
new file mode 100644
index 0000000000..1f7cbbf029
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreEntryImporter.java
@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.Beta;
+
+import java.io.InputStream;
+
+/**
+ * The {@link StoreEntryImporter} is used to import a store entry from an {@link InputStream}.
+ *
This interface is not yet finalized and might change in the upcoming versions.
+ *
+ * @since 2.13.0
+ */
+@Beta
+public interface StoreEntryImporter {
+
+ /**
+ * Will be called for each entry of the store.
+ * @param name The name of the store entry.
+ * @param stream An input stream with the raw data of the store entry.
+ */
+ void importEntry(String name, InputStream stream);
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreEntryImporterFactory.java b/scm-core/src/main/java/sonia/scm/store/StoreEntryImporterFactory.java
new file mode 100644
index 0000000000..2c4525f750
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreEntryImporterFactory.java
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Create a {@link StoreEntryImporter} for the store type and store name.
+ *
This interface is not yet finalized and might change in the upcoming versions.
+ *
+ * @since 2.13.0
+ */
+@Beta
+public interface StoreEntryImporterFactory {
+ /**
+ * Returns a {@link StoreEntryImporter}.
+ *
+ * @param metaData The metaData about this store. For example store type and store name.
+ */
+ StoreEntryImporter importStore(StoreEntryMetaData metaData);
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreEntryMetaData.java b/scm-core/src/main/java/sonia/scm/store/StoreEntryMetaData.java
new file mode 100644
index 0000000000..54f59ea291
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreEntryMetaData.java
@@ -0,0 +1,33 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import lombok.Value;
+
+@Value
+public class StoreEntryMetaData {
+ StoreType type;
+ String name;
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreExporter.java b/scm-core/src/main/java/sonia/scm/store/StoreExporter.java
new file mode 100644
index 0000000000..200abae0c0
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreExporter.java
@@ -0,0 +1,42 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.Beta;
+import sonia.scm.repository.Repository;
+
+import java.util.List;
+
+/**
+ * The {@link StoreExporter} is used to collect all {@link ExportableStore}s for a given repository.
+ * An {@link ExportableStore} can be used to export all data which is stored inside.
+ *
This interface is not yet finalized and might change in the upcoming versions.
+ *
+ * @since 2.13.0
+ */
+@Beta
+public interface StoreExporter {
+ List listExportableStores(Repository repository);
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreImporter.java b/scm-core/src/main/java/sonia/scm/store/StoreImporter.java
new file mode 100644
index 0000000000..dbcc6bda39
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreImporter.java
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.Beta;
+import sonia.scm.repository.Repository;
+
+/**
+ * The {@link StoreImporter} is used to create a {@link StoreEntryImporterFactory} for a {@link Repository}.
+ *
This interface is not yet finalized and might change in the upcoming versions.
+ *
+ * @since 2.13.0
+ */
+@Beta
+public interface StoreImporter {
+ /**
+ * Returns a {@link StoreEntryImporterFactory} for the {@link Repository}
+ *
+ * @param repository
+ */
+ StoreEntryImporterFactory doImport(Repository repository);
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreType.java b/scm-core/src/main/java/sonia/scm/store/StoreType.java
new file mode 100644
index 0000000000..982eb71f4b
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreType.java
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+public enum StoreType {
+
+ DATA("data"),
+ CONFIG("config"),
+ BLOB("blob"),
+ CONFIG_ENTRY("configEntry");
+
+ StoreType(String value) {
+ this.value = value;
+ }
+
+ private final String value;
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/update/RepositoryUpdateIterator.java b/scm-core/src/main/java/sonia/scm/update/RepositoryUpdateIterator.java
new file mode 100644
index 0000000000..7ac218bcd7
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/update/RepositoryUpdateIterator.java
@@ -0,0 +1,42 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.update;
+
+import java.util.function.Consumer;
+
+/**
+ * Implementations of this interface can be used to iterate all repositories in update steps.
+ *
+ * @since 2.13.0
+ */
+public interface RepositoryUpdateIterator {
+
+ /**
+ * Calls the given consumer with each repository id.
+ *
+ * @since 2.13.0
+ */
+ void forEachRepository(Consumer repositoryIdConsumer);
+}
diff --git a/scm-core/src/main/java/sonia/scm/update/StoreUpdateStepUtilFactory.java b/scm-core/src/main/java/sonia/scm/update/StoreUpdateStepUtilFactory.java
new file mode 100644
index 0000000000..f42e0ed3c3
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/update/StoreUpdateStepUtilFactory.java
@@ -0,0 +1,93 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.update;
+
+import sonia.scm.store.StoreParameters;
+import sonia.scm.store.StoreType;
+
+public interface StoreUpdateStepUtilFactory {
+
+ default UtilForTypeBuilder forType(StoreType type) {
+ return new UtilForTypeBuilder(this, type);
+ }
+
+ final class UtilForTypeBuilder {
+ private final StoreUpdateStepUtilFactory factory;
+ private final StoreType type;
+
+ public UtilForTypeBuilder(StoreUpdateStepUtilFactory factory, StoreType type) {
+ this.factory = factory;
+ this.type = type;
+ }
+
+ public UtilForNameBuilder forName(String name) {
+ return new UtilForNameBuilder(factory, type, name);
+ }
+ }
+
+ final class UtilForNameBuilder {
+
+ private final StoreUpdateStepUtilFactory factory;
+ private final StoreType type;
+ private final String name;
+ private String repositoryId;
+
+ public UtilForNameBuilder(StoreUpdateStepUtilFactory factory, StoreType type, String name) {
+ this.factory = factory;
+ this.type = type;
+ this.name = name;
+ }
+
+ public UtilForNameBuilder forRepository(String repositoryId) {
+ this.repositoryId = repositoryId;
+ return this;
+ }
+
+ public StoreUpdateStepUtil build() {
+ return factory.build(
+ type,
+ new StoreParameters() {
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getRepositoryId() {
+ return repositoryId;
+ }
+ }
+ );
+ }
+ }
+
+ StoreUpdateStepUtil build(StoreType type, StoreParameters parameters);
+
+ interface StoreUpdateStepUtil {
+ void renameStore(String newName);
+
+ void deleteStore();
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportCopier.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportCopier.java
new file mode 100644
index 0000000000..460c7fff0f
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportCopier.java
@@ -0,0 +1,52 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.repository.api.ExportFailedException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static sonia.scm.ContextEntry.ContextBuilder.noContext;
+
+final class ExportCopier {
+
+ private ExportCopier() {
+ }
+
+ static void putFileContentIntoStream(Exporter exporter, Path file) {
+ try (OutputStream stream = exporter.put(file.getFileName().toString(), Files.size(file))) {
+ Files.copy(file, stream);
+ } catch (IOException e) {
+ throw new ExportFailedException(
+ noContext(),
+ "Could not copy file to export stream: " + file,
+ e
+ );
+ }
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java
new file mode 100644
index 0000000000..d2b45837f3
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java
@@ -0,0 +1,51 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+
+class ExportableBlobFileStore extends ExportableDirectoryBasedFileStore {
+
+ static Function>> BLOB_FACTORY =
+ storeType -> storeType == StoreType.BLOB ? of(ExportableBlobFileStore::new) : empty();
+
+ ExportableBlobFileStore(Path directory) {
+ super(directory);
+ }
+
+ @Override
+ StoreType getStoreType() {
+ return StoreType.BLOB;
+ }
+
+ boolean shouldIncludeFile(Path file) {
+ return file.getFileName().toString().endsWith(".blob");
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java
new file mode 100644
index 0000000000..b2decc4cf0
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java
@@ -0,0 +1,58 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+import static sonia.scm.store.ExportCopier.putFileContentIntoStream;
+
+class ExportableConfigFileStore implements ExportableStore {
+
+ private final Path file;
+
+ static Function>> CONFIG_FACTORY =
+ storeType -> storeType == StoreType.CONFIG ? of(ExportableConfigFileStore::new) : empty();
+
+ ExportableConfigFileStore(Path file) {
+ this.file = file;
+ }
+
+ @Override
+ public StoreEntryMetaData getMetaData() {
+ return new StoreEntryMetaData(StoreType.CONFIG, file.getFileName().toString());
+ }
+
+ @Override
+ public void export(Exporter exporter) throws IOException {
+ if (file.getFileName().toString().endsWith(".xml")) {
+ putFileContentIntoStream(exporter, file);
+ }
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java
new file mode 100644
index 0000000000..4841775e49
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java
@@ -0,0 +1,51 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+
+class ExportableDataFileStore extends ExportableDirectoryBasedFileStore {
+
+ static Function>> DATA_FACTORY =
+ storeType -> storeType == StoreType.DATA ? of(ExportableDataFileStore::new) : empty();
+
+ ExportableDataFileStore(Path directory) {
+ super(directory);
+ }
+
+ @Override
+ StoreType getStoreType() {
+ return StoreType.DATA;
+ }
+
+ boolean shouldIncludeFile(Path file) {
+ return file.getFileName().toString().endsWith(".xml");
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDirectoryBasedFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDirectoryBasedFileStore.java
new file mode 100644
index 0000000000..47dfd32830
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDirectoryBasedFileStore.java
@@ -0,0 +1,76 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.repository.api.ExportFailedException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import static sonia.scm.ContextEntry.ContextBuilder.noContext;
+import static sonia.scm.store.ExportCopier.putFileContentIntoStream;
+
+abstract class ExportableDirectoryBasedFileStore implements ExportableStore {
+
+ private final Path directory;
+
+ ExportableDirectoryBasedFileStore(Path directory) {
+ this.directory = directory;
+ }
+
+ @Override
+ public StoreEntryMetaData getMetaData() {
+ return new StoreEntryMetaData(getStoreType(), directory.getFileName().toString());
+ }
+
+ abstract StoreType getStoreType();
+
+ abstract boolean shouldIncludeFile(Path file);
+
+ @Override
+ public void export(Exporter exporter) throws IOException {
+ exportDirectoryEntries(exporter, directory);
+ }
+
+ private void exportDirectoryEntries(Exporter exporter, Path directory) {
+ try (Stream fileList = Files.list(directory)) {
+ fileList.forEach(fileOrDir -> exportIfRelevant(exporter, fileOrDir));
+ } catch (IOException e) {
+ throw new ExportFailedException(
+ noContext(),
+ "Could not read directory " + directory,
+ e
+ );
+ }
+ }
+
+ private void exportIfRelevant(Exporter exporter, Path fileOrDir) {
+ if (!Files.isDirectory(fileOrDir) && shouldIncludeFile(fileOrDir)) {
+ putFileContentIntoStream(exporter, fileOrDir);
+ }
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreEntryImporter.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreEntryImporter.java
new file mode 100644
index 0000000000..040892abb5
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreEntryImporter.java
@@ -0,0 +1,62 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import com.google.common.annotations.VisibleForTesting;
+import sonia.scm.ContextEntry;
+import sonia.scm.repository.api.ImportFailedException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class FileBasedStoreEntryImporter implements StoreEntryImporter {
+
+ private final Path directory;
+
+ FileBasedStoreEntryImporter(Path directory) {
+ this.directory = directory;
+ }
+
+ @VisibleForTesting
+ Path getDirectory() {
+ return this.directory;
+ }
+
+ @Override
+ public void importEntry(String name, InputStream stream) {
+ Path filePath = directory.resolve(name);
+ try {
+ Files.copy(stream, filePath);
+ } catch (IOException e) {
+ throw new ImportFailedException(
+ ContextEntry.ContextBuilder.noContext(),
+ String.format("Could not import file %s for store %s", name, directory.toString()),
+ e
+ );
+ }
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreEntryImporterFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreEntryImporterFactory.java
new file mode 100644
index 0000000000..470055ef0b
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreEntryImporterFactory.java
@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.ContextEntry;
+import sonia.scm.repository.api.ImportFailedException;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+class FileBasedStoreEntryImporterFactory implements StoreEntryImporterFactory {
+
+ private final Path directory;
+
+ FileBasedStoreEntryImporterFactory(Path directory) {
+ this.directory = directory;
+ }
+
+ @Override
+ public StoreEntryImporter importStore(StoreEntryMetaData metaData) {
+ StoreType storeType = metaData.getType();
+ String storeName = metaData.getName();
+ Path storeDirectory = directory.resolve(Store.STORE_DIRECTORY);
+ try {
+ storeDirectory = storeDirectory.resolve(resolveFilePath(storeType.getValue(), storeName));
+ Files.createDirectories(storeDirectory);
+ if (!Files.exists(storeDirectory)) {
+ throw new ImportFailedException(
+ ContextEntry.ContextBuilder.noContext(),
+ String.format("Could not create store for type %s and name %s", storeType, storeName)
+ );
+ }
+ return new FileBasedStoreEntryImporter(storeDirectory);
+
+ } catch (IOException e) {
+ throw new ImportFailedException(
+ ContextEntry.ContextBuilder.noContext(),
+ String.format("Could not create store directory %s for type %s and name %s", storeDirectory, storeType, storeName)
+ );
+ }
+ }
+
+ private Path resolveFilePath(String type, String name) {
+ if (name == null || name.isEmpty()) {
+ return Paths.get(type);
+ }
+ return Paths.get(type, name);
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileRepositoryUpdateIterator.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileRepositoryUpdateIterator.java
new file mode 100644
index 0000000000..11278b4729
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileRepositoryUpdateIterator.java
@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.update.RepositoryUpdateIterator;
+
+import javax.inject.Inject;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+
+public class FileRepositoryUpdateIterator implements RepositoryUpdateIterator {
+
+ private final RepositoryLocationResolver locationResolver;
+
+ @Inject
+ public FileRepositoryUpdateIterator(RepositoryLocationResolver locationResolver) {
+ this.locationResolver = locationResolver;
+ }
+
+ @Override
+ public void forEachRepository(Consumer repositoryIdConsumer) {
+ locationResolver
+ .forClass(Path.class)
+ .forAllLocations((repositoryId, path) -> repositoryIdConsumer.accept(repositoryId));
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java
new file mode 100644
index 0000000000..c81e76c46d
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java
@@ -0,0 +1,121 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.repository.api.ExportFailedException;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static sonia.scm.ContextEntry.ContextBuilder.noContext;
+import static sonia.scm.store.ExportableBlobFileStore.BLOB_FACTORY;
+import static sonia.scm.store.ExportableConfigFileStore.CONFIG_FACTORY;
+import static sonia.scm.store.ExportableDataFileStore.DATA_FACTORY;
+
+public class FileStoreExporter implements StoreExporter {
+
+ private final RepositoryLocationResolver locationResolver;
+
+ private static final Collection>>> STORE_FACTORIES =
+ asList(DATA_FACTORY, BLOB_FACTORY, CONFIG_FACTORY);
+
+ @Inject
+ public FileStoreExporter(RepositoryLocationResolver locationResolver) {
+ this.locationResolver = locationResolver;
+ }
+
+ @Override
+ public List listExportableStores(Repository repository) {
+ List exportableStores = new ArrayList<>();
+ Path storeDirectory = resolveStoreDirectory(repository);
+ if (!Files.exists(storeDirectory)) {
+ return emptyList();
+ }
+ try (Stream storeTypeDirectories = Files.list(storeDirectory)) {
+ storeTypeDirectories.forEach(storeTypeDirectory ->
+ exportStoreTypeDirectories(exportableStores, storeTypeDirectory)
+ );
+ } catch (IOException e) {
+ throw new ExportFailedException(
+ noContext(),
+ "Could not list content of directory " + storeDirectory,
+ e
+ );
+ }
+ return exportableStores;
+ }
+
+ private Path resolveStoreDirectory(Repository repository) {
+ return locationResolver
+ .forClass(Path.class)
+ .getLocation(repository.getId())
+ .resolve(Store.STORE_DIRECTORY);
+ }
+
+ private void exportStoreTypeDirectories(List exportableStores, Path storeTypeDirectory) {
+ try (Stream storeDirectories = Files.list(storeTypeDirectory)) {
+ storeDirectories.forEach(storeDirectory ->
+ getStoreFor(storeDirectory).ifPresent(exportableStores::add)
+ );
+ } catch (IOException e) {
+ throw new ExportFailedException(
+ noContext(),
+ "Could not list content of directory " + storeTypeDirectory,
+ e
+ );
+ }
+ }
+
+ private Optional getStoreFor(Path storePath) {
+ return STORE_FACTORIES
+ .stream()
+ .map(factory -> factory.apply(getEnumForValue(storePath.getParent())))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst()
+ .map(f -> f.apply(storePath));
+ }
+
+ private StoreType getEnumForValue(Path storeTypeDirectory) {
+ for (StoreType type : StoreType.values()) {
+ if (type.getValue().equals(storeTypeDirectory.getFileName().toString())) {
+ return type;
+ }
+ }
+ return null;
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreUpdateStepUtil.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreUpdateStepUtil.java
new file mode 100644
index 0000000000..ee8ce5df61
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreUpdateStepUtil.java
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.SCMContextProvider;
+import sonia.scm.migration.UpdateException;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.update.StoreUpdateStepUtilFactory;
+import sonia.scm.util.IOUtil;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class FileStoreUpdateStepUtil implements StoreUpdateStepUtilFactory.StoreUpdateStepUtil {
+
+ private final RepositoryLocationResolver locationResolver;
+ private final SCMContextProvider contextProvider;
+
+ private final StoreParameters parameters;
+ private final StoreType type;
+
+ FileStoreUpdateStepUtil(RepositoryLocationResolver locationResolver, SCMContextProvider contextProvider, StoreParameters parameters, StoreType type) {
+ this.locationResolver = locationResolver;
+ this.contextProvider = contextProvider;
+ this.parameters = parameters;
+ this.type = type;
+ }
+
+ @Override
+ public void renameStore(String newName) {
+ Path oldStorePath = resolveBasePath().resolve(parameters.getName());
+ if (Files.exists(oldStorePath)) {
+ Path newStorePath = resolveBasePath().resolve(newName);
+ try {
+ Files.move(oldStorePath, newStorePath);
+ } catch (IOException e) {
+ throw new UpdateException(String.format("Could not move store path %s to %s", oldStorePath, newStorePath), e);
+ }
+ }
+ }
+
+ @Override
+ public void deleteStore() {
+ Path oldStorePath = resolveBasePath().resolve(parameters.getName());
+ IOUtil.deleteSilently(oldStorePath.toFile());
+ }
+
+ private Path resolveBasePath() {
+ Path basePath;
+ if (parameters.getRepositoryId() != null) {
+ basePath = locationResolver.forClass(Path.class).getLocation(parameters.getRepositoryId());
+ } else {
+ basePath = contextProvider.getBaseDirectory().toPath();
+ }
+ Path storeBasePath;
+ if (parameters.getRepositoryId() == null) {
+ storeBasePath = basePath.resolve(Store.forStoreType(type).getGlobalStoreDirectory());
+ } else {
+ storeBasePath = basePath.resolve(Store.forStoreType(type).getRepositoryStoreDirectory());
+ }
+ return storeBasePath;
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreUpdateStepUtilFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreUpdateStepUtilFactory.java
new file mode 100644
index 0000000000..bae718cd8f
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreUpdateStepUtilFactory.java
@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.update.StoreUpdateStepUtilFactory;
+
+import javax.inject.Inject;
+
+public class FileStoreUpdateStepUtilFactory implements StoreUpdateStepUtilFactory {
+
+ private final RepositoryLocationResolver locationResolver;
+ private final SCMContextProvider contextProvider;
+
+ @Inject
+ public FileStoreUpdateStepUtilFactory(RepositoryLocationResolver locationResolver, SCMContextProvider contextProvider) {
+ this.locationResolver = locationResolver;
+ this.contextProvider = contextProvider;
+ }
+
+ @Override
+ public StoreUpdateStepUtil build(StoreType type, StoreParameters parameters) {
+ return new FileStoreUpdateStepUtil(locationResolver, contextProvider, parameters, type);
+ }
+
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/RepositoryStoreImporter.java b/scm-dao-xml/src/main/java/sonia/scm/store/RepositoryStoreImporter.java
new file mode 100644
index 0000000000..927510175b
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/RepositoryStoreImporter.java
@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
+
+import javax.inject.Inject;
+import java.nio.file.Path;
+
+public class RepositoryStoreImporter implements StoreImporter {
+
+ private final RepositoryLocationResolver locationResolver;
+
+ @Inject
+ public RepositoryStoreImporter(RepositoryLocationResolver locationResolver) {
+ this.locationResolver = locationResolver;
+ }
+
+ @Override
+ public StoreEntryImporterFactory doImport(Repository repository) {
+ Path storeLocation = locationResolver
+ .forClass(Path.class)
+ .getLocation(repository.getId());
+ return new FileBasedStoreEntryImporterFactory(storeLocation);
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/Store.java b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java
index cc259c7e55..5c134dcfee 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/Store.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java
@@ -21,18 +21,32 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.store;
import java.io.File;
-public enum Store {
+enum Store {
CONFIG("config"),
DATA("data"),
BLOB("blob");
private static final String GLOBAL_STORE_BASE_DIRECTORY = "var";
- private static final String STORE_DIRECTORY = "store";
+ static final String STORE_DIRECTORY = "store";
+
+ static Store forStoreType(StoreType storeType) {
+ switch (storeType) {
+ case BLOB:
+ return BLOB;
+ case DATA:
+ return DATA;
+ case CONFIG:
+ case CONFIG_ENTRY:
+ return CONFIG;
+ default:
+ throw new IllegalArgumentException("unknown store type: " + storeType);
+ }
+ }
private String directory;
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java
new file mode 100644
index 0000000000..25437031b8
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java
@@ -0,0 +1,144 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ExportableFileStoreTest {
+
+ @Mock
+ Exporter exporter;
+
+ @Test
+ void shouldNotPutContentIfNoFilesExists(@TempDir Path temp) throws IOException {
+ Path dataStoreDir = temp.resolve("some-store");
+ Files.createDirectories(dataStoreDir);
+
+ ExportableStore exportableFileStore = new ExportableDataFileStore(dataStoreDir);
+ exportableFileStore.export(exporter);
+
+ verify(exporter, never()).put(anyString(), anyLong());
+ }
+
+ @Test
+ void shouldPutContentIntoExporterForDataStore(@TempDir Path temp) throws IOException {
+ createFile(temp, "data", "trace", "first.xml");
+ createFile(temp, "data", "trace", "second.xml");
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ExportableStore exportableFileStore = new ExportableDataFileStore(temp.resolve("data").resolve("trace"));
+ when(exporter.put(anyString(), anyLong())).thenReturn(os);
+
+ exportableFileStore.export(exporter);
+
+ verify(exporter).put(eq("first.xml"), anyLong());
+ verify(exporter).put(eq("second.xml"), anyLong());
+ assertThat(os.toString()).isNotBlank();
+ }
+
+ @Test
+ void shouldPutContentIntoExporterForConfigStore(@TempDir Path temp) throws IOException {
+ createFile(temp, "config", "", "first.xml");
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ExportableStore exportableConfigFileStore = new ExportableConfigFileStore(temp.resolve("config").resolve("first.xml"));
+ when(exporter.put(anyString(), anyLong())).thenReturn(os);
+
+ exportableConfigFileStore.export(exporter);
+
+ verify(exporter).put(eq("first.xml"), anyLong());
+ assertThat(os.toString()).isNotBlank();
+ }
+
+ @Test
+ void shouldFilterNoneConfigFiles(@TempDir Path temp) throws IOException {
+ createFile(temp, "config", "", "first.bck");
+ ExportableStore exportableConfigFileStore = new ExportableConfigFileStore(temp.resolve("config").resolve("first.bck"));
+
+ exportableConfigFileStore.export(exporter);
+
+ verify(exporter, never()).put(anyString(), anyLong());
+ }
+
+ @Test
+ void shouldPutContentIntoExporterForBlobStore(@TempDir Path temp) throws IOException {
+ createFile(temp, "blob", "assets", "first.blob");
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Exporter exporter = mock(Exporter.class);
+ ExportableStore exportableBlobFileStore = new ExportableBlobFileStore(temp.resolve("blob").resolve("assets"));
+ when(exporter.put(anyString(), anyLong())).thenReturn(os);
+
+ exportableBlobFileStore.export(exporter);
+
+ verify(exporter).put(eq("first.blob"), anyLong());
+ assertThat(os.toString()).isNotBlank();
+ }
+
+ @Test
+ void shouldSkipFilteredBlobFiles(@TempDir Path temp) throws IOException {
+ createFile(temp, "blob", "security", "second.xml");
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Exporter exporter = mock(Exporter.class);
+ ExportableStore exportableBlobFileStore = new ExportableBlobFileStore(temp.resolve("blob").resolve("security"));
+
+ exportableBlobFileStore.export(exporter);
+
+ verify(exporter, never()).put(anyString(), anyLong());
+ assertThat(os.toString()).isBlank();
+ }
+
+ private File createFile(Path temp, String type, String name, String fileName) throws IOException {
+ Path path = name != null ? temp.resolve(type).resolve(name) : temp.resolve(type);
+
+ new File(path.toUri()).mkdirs();
+ File file = new File(path.toFile(), fileName);
+ if (!file.exists()) {
+ file.createNewFile();
+ }
+ FileWriter source = new FileWriter(file);
+ source.write("something");
+ source.close();
+ return file;
+ }
+}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBasedStoreEntryImporterFactoryTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBasedStoreEntryImporterFactoryTest.java
new file mode 100644
index 0000000000..c532f087fa
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBasedStoreEntryImporterFactoryTest.java
@@ -0,0 +1,50 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package sonia.scm.store;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FileBasedStoreEntryImporterFactoryTest {
+
+ @Test
+ void shouldCreateStoreEntryImporterForDataStore(@TempDir Path temp) {
+ FileBasedStoreEntryImporterFactory factory = new FileBasedStoreEntryImporterFactory(temp);
+
+ FileBasedStoreEntryImporter dataImporter = (FileBasedStoreEntryImporter) factory.importStore(new StoreEntryMetaData(StoreType.DATA, "hitchhiker"));
+ assertThat(dataImporter.getDirectory()).isEqualTo(temp.resolve("store").resolve("data").resolve("hitchhiker"));
+ }
+
+ @Test
+ void shouldCreateStoreEntryImporterForConfigStore(@TempDir Path temp) {
+ FileBasedStoreEntryImporterFactory factory = new FileBasedStoreEntryImporterFactory(temp);
+
+ FileBasedStoreEntryImporter configImporter = (FileBasedStoreEntryImporter) factory.importStore(new StoreEntryMetaData(StoreType.CONFIG, ""));
+ assertThat(configImporter.getDirectory()).isEqualTo(temp.resolve("store").resolve("config"));
+ }
+}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBasedStoreEntryImporterTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBasedStoreEntryImporterTest.java
new file mode 100644
index 0000000000..00099e0150
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBasedStoreEntryImporterTest.java
@@ -0,0 +1,50 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FileBasedStoreEntryImporterTest {
+
+ @Test
+ void shouldCreateFileFromInputStream(@TempDir Path temp) {
+ FileBasedStoreEntryImporter importer = new FileBasedStoreEntryImporter(temp);
+ String fileName = "testStore.xml";
+
+ importer.importEntry(fileName, new ByteArrayInputStream("testdata".getBytes()));
+
+ assertThat(Files.exists(temp.resolve(fileName))).isTrue();
+ assertThat(temp.resolve(fileName)).hasContent("testdata");
+ }
+}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java
new file mode 100644
index 0000000000..d7677e94a4
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java
@@ -0,0 +1,90 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Answers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.repository.RepositoryTestData;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class FileStoreExporterTest {
+
+ private static final Repository REPOSITORY = RepositoryTestData.create42Puzzle();
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private RepositoryLocationResolver resolver;
+
+ @InjectMocks
+ private FileStoreExporter fileStoreExporter;
+
+ @Test
+ void shouldReturnEmptyList(@TempDir Path temp) {
+ when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp);
+
+ List exportableStores = fileStoreExporter.listExportableStores(REPOSITORY);
+ assertThat(exportableStores).isEmpty();
+ }
+
+ @Test
+ void shouldReturnListOfExportableStores(@TempDir Path temp) throws IOException {
+ Path storePath = temp.resolve("store");
+ createFile(storePath, StoreType.CONFIG.getValue(), null, "first.xml");
+ createFile(storePath, StoreType.DATA.getValue(), "ci", "second.xml");
+ createFile(storePath, StoreType.DATA.getValue(), "jenkins", "third.xml");
+ when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp);
+
+ List exportableStores = fileStoreExporter.listExportableStores(REPOSITORY);
+
+ assertThat(exportableStores).hasSize(3);
+ assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.CONFIG))).hasSize(1);
+ assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.DATA))).hasSize(2);
+ }
+
+ private void createFile(Path storePath, String type, String name, String fileName) throws IOException {
+ Path path = name != null ? storePath.resolve(type).resolve(name) : storePath.resolve(type);
+ Files.createDirectories(path);
+ Path file = path.resolve(fileName);
+ if (!Files.exists(file)) {
+ Files.createFile(file);
+ }
+ Files.write(file, Collections.singletonList("something"));
+ }
+}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreUpdateStepUtilFactoryTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreUpdateStepUtilFactoryTest.java
new file mode 100644
index 0000000000..570141239f
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreUpdateStepUtilFactoryTest.java
@@ -0,0 +1,121 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.update.StoreUpdateStepUtilFactory;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.lenient;
+
+@ExtendWith(MockitoExtension.class)
+class FileStoreUpdateStepUtilFactoryTest {
+
+ @Mock
+ private RepositoryLocationResolver locationResolver;
+ @Mock
+ private RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance;
+ @Mock
+ private SCMContextProvider contextProvider;
+
+ @InjectMocks
+ private FileStoreUpdateStepUtilFactory factory;
+
+ private Path globalPath;
+ private Path repositoryPath;
+
+ @BeforeEach
+ void initPaths(@TempDir Path temp) throws IOException {
+ globalPath = temp.resolve("global");
+ Files.createDirectories(globalPath);
+ lenient().doReturn(globalPath.toFile()).when(contextProvider).getBaseDirectory();
+
+ repositoryPath = temp.resolve("repo");
+ Files.createDirectories(repositoryPath);
+ lenient().doReturn(true).when(locationResolver).supportsLocationType(Path.class);
+ lenient().doReturn(locationResolverInstance).when(locationResolver).forClass(Path.class);
+ lenient().doReturn(repositoryPath).when(locationResolverInstance).getLocation("repo-id");
+ }
+
+ @Test
+ void shouldMoveGlobalDataDirectory() throws IOException {
+ Path dataPath = globalPath.resolve("var").resolve("data");
+ Files.createDirectories(dataPath.resolve("something"));
+ Files.createFile(dataPath.resolve("something").resolve("some.file"));
+ StoreUpdateStepUtilFactory.StoreUpdateStepUtil util =
+ factory
+ .forType(StoreType.DATA)
+ .forName("something")
+ .build();
+
+ util.renameStore("new-name");
+
+ assertThat(dataPath.resolve("new-name").resolve("some.file")).exists();
+ assertThat(dataPath.resolve("something")).doesNotExist();
+ }
+
+ @Test
+ void shouldMoveRepositoryDataDirectory() throws IOException {
+ Path dataPath = repositoryPath.resolve("store").resolve("data");
+ Files.createDirectories(dataPath.resolve("something"));
+ Files.createFile(dataPath.resolve("something").resolve("some.file"));
+ StoreUpdateStepUtilFactory.StoreUpdateStepUtil util =
+ factory
+ .forType(StoreType.DATA)
+ .forName("something")
+ .forRepository("repo-id")
+ .build();
+
+ util.renameStore("new-name");
+
+ assertThat(dataPath.resolve("new-name").resolve("some.file")).exists();
+ assertThat(dataPath.resolve("something")).doesNotExist();
+ }
+
+ @Test
+ void shouldHandleMissingMoveGlobalDataDirectory() throws IOException {
+ StoreUpdateStepUtilFactory.StoreUpdateStepUtil util =
+ factory
+ .forType(StoreType.DATA)
+ .forName("something")
+ .build();
+
+ util.renameStore("new-name");
+
+ assertThat(globalPath.resolve("new-name")).doesNotExist();
+ }
+}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/RepositoryStoreImporterTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/RepositoryStoreImporterTest.java
new file mode 100644
index 0000000000..bc1e18e696
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/RepositoryStoreImporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package sonia.scm.store;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Answers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.repository.RepositoryTestData;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class RepositoryStoreImporterTest {
+ private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private RepositoryLocationResolver locationResolver;
+
+ @InjectMocks
+ private RepositoryStoreImporter repositoryStoreImporter;
+
+ @Test
+ void shouldImportStore() {
+ StoreEntryImporterFactory storeEntryImporterFactory = repositoryStoreImporter.doImport(REPOSITORY);
+ assertThat(storeEntryImporterFactory).isInstanceOf(StoreEntryImporterFactory.class);
+ }
+}
diff --git a/scm-dao-xml/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-dao-xml/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/scm-dao-xml/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java
index b37b23208a..4d87ea188a 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.web.lfs;
import com.google.inject.Inject;
@@ -32,44 +32,37 @@ import sonia.scm.store.BlobStoreFactory;
/**
* Creates {@link BlobStore} objects to store lfs objects.
- *
+ *
* @author Sebastian Sdorra
* @since 1.54
*/
@Singleton
public class LfsBlobStoreFactory {
-
- private static final String GIT_LFS_REPOSITORY_POSTFIX = "-git-lfs";
-
+
+ private static final String GIT_LFS_STORE_NAME = "git-lfs";
+
private final BlobStoreFactory blobStoreFactory;
/**
* Create a new instance.
- *
+ *
* @param blobStoreFactory blob store factory
*/
@Inject
public LfsBlobStoreFactory(BlobStoreFactory blobStoreFactory) {
this.blobStoreFactory = blobStoreFactory;
}
-
+
/**
* Provides a {@link BlobStore} corresponding to the SCM Repository.
- *
- * git-lfs repositories should generally carry the same name as their regular SCM repository counterparts. However,
- * we have decided to store them under their IDs instead of their names, since the names might change and provide
- * other drawbacks, as well.
- *
- * These repositories will have {@linkplain #GIT_LFS_REPOSITORY_POSTFIX} appended to their IDs.
*
* @param repository The SCM Repository to provide a LFS {@link BlobStore} for.
- *
+ *
* @return blob store for the corresponding scm repository
*/
- @SuppressWarnings("unchecked")
public BlobStore getLfsBlobStore(Repository repository) {
return blobStoreFactory
- .withName(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX)
+ .withName(GIT_LFS_STORE_NAME)
.forRepository(repository)
.build();
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/RemoveRepositoryIdFromBlobStoreUpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/RemoveRepositoryIdFromBlobStoreUpdateStep.java
new file mode 100644
index 0000000000..919a656b16
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/RemoveRepositoryIdFromBlobStoreUpdateStep.java
@@ -0,0 +1,73 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.web.lfs;
+
+import sonia.scm.migration.UpdateStep;
+import sonia.scm.plugin.Extension;
+import sonia.scm.store.StoreType;
+import sonia.scm.update.RepositoryUpdateIterator;
+import sonia.scm.update.StoreUpdateStepUtilFactory;
+import sonia.scm.version.Version;
+
+import javax.inject.Inject;
+
+import static sonia.scm.version.Version.parse;
+
+@Extension
+public class RemoveRepositoryIdFromBlobStoreUpdateStep implements UpdateStep {
+
+ private final RepositoryUpdateIterator repositoryUpdateIterator;
+ private final StoreUpdateStepUtilFactory utilFactory;
+
+ @Inject
+ public RemoveRepositoryIdFromBlobStoreUpdateStep(RepositoryUpdateIterator repositoryUpdateIterator, StoreUpdateStepUtilFactory utilFactory) {
+ this.repositoryUpdateIterator = repositoryUpdateIterator;
+ this.utilFactory = utilFactory;
+ }
+
+ @Override
+ public void doUpdate() {
+ repositoryUpdateIterator.forEachRepository(this::doUpdate);
+ }
+
+ private void doUpdate(String repositoryId) {
+ utilFactory
+ .forType(StoreType.BLOB)
+ .forName(repositoryId + "-git-lfs")
+ .forRepository(repositoryId)
+ .build()
+ .renameStore("git-lfs");
+ }
+
+ @Override
+ public Version getTargetVersion() {
+ return parse("2.0.1");
+ }
+
+ @Override
+ public String getAffectedDataType() {
+ return "sonia.scm.git.lfs";
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java
index 180612a207..143f60caa6 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java
@@ -62,7 +62,7 @@ public class LfsBlobStoreFactoryTest {
// just make sure the right parameter is passed, as properly validating the return value is nearly impossible with
// the return value (and should not be part of this test)
verify(blobStoreFactory).getStore(argThat(blobStoreParameters -> {
- assertThat(blobStoreParameters.getName()).isEqualTo("the-id-git-lfs");
+ assertThat(blobStoreParameters.getName()).isEqualTo("git-lfs");
assertThat(blobStoreParameters.getRepositoryId()).isEqualTo("the-id");
return true;
}));
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/RemoveRepositoryIdFromBlobStoreUpdateStepTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/RemoveRepositoryIdFromBlobStoreUpdateStepTest.java
new file mode 100644
index 0000000000..bef6cb6a68
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/RemoveRepositoryIdFromBlobStoreUpdateStepTest.java
@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.web.lfs;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Answers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.store.StoreType;
+import sonia.scm.update.RepositoryUpdateIterator;
+import sonia.scm.update.StoreUpdateStepUtilFactory;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class RemoveRepositoryIdFromBlobStoreUpdateStepTest {
+
+ @Mock
+ private RepositoryUpdateIterator repositoryUpdateIterator;
+ @Mock(answer = Answers.CALLS_REAL_METHODS)
+ private StoreUpdateStepUtilFactory utilFactory;
+ @Mock
+ private StoreUpdateStepUtilFactory.StoreUpdateStepUtil util;
+
+ @InjectMocks
+ private RemoveRepositoryIdFromBlobStoreUpdateStep updateStep;
+
+ @Test
+ void migrateBlobsFromOldStoreToNewStore() throws IOException {
+ Mockito.doAnswer(invocation -> {
+ invocation.getArgument(0, Consumer.class).accept("repo-id");
+ return null;
+ }).when(repositoryUpdateIterator).forEachRepository(any());
+
+ doReturn(util)
+ .when(utilFactory).build(eq(StoreType.BLOB), argThat(argument -> argument.getName().equals("repo-id-git-lfs")));
+
+ updateStep.doUpdate();
+
+ verify(util).renameStore("git-lfs");
+ }
+}
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index 289ca66d30..a5f60986f7 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -75,6 +75,10 @@
"title": "Wählen Sie Ihre Datei aus",
"helpText": "Wählen Sie die Datei aus der das Repository importiert werden soll."
},
+ "fullImport": {
+ "title": "SCM-Manager Repository Archiv",
+ "helpText": "Wählen Sie das Repository Archiv aus. Das Archiv muss von einer SCM-Manager Instanz exportiert worden sein."
+ },
"pending": {
"subtitle": "Repository wird importiert...",
"infoText": "Ihr Repository wird gerade importiert. Dies kann einen Moment dauern. Sie werden weitergeleitet, sobald der Import abgeschlossen ist. Wenn Sie diese Seite verlassen, können Sie nicht zurückkehren, um den Import-Status zu erfahren."
@@ -87,7 +91,11 @@
},
"bundle": {
"label": "Import aus Dump",
- "helpText": "Das Repository wird aus einen Datei Dump importiert."
+ "helpText": "Das Repository wird aus einem Datei Dump importiert."
+ },
+ "fullImport": {
+ "label": "Import aus Archiv mit Metadaten (Experimentell)",
+ "helpText": "Das Repository inkl. der Metadaten wird aus einem archivierten Dump importiert. Das Import Archiv muss von einem anderen SCM-Manager generiert worden sein."
}
}
},
@@ -248,6 +256,10 @@
"label": "Komprimieren",
"helpText": "Export Datei vor dem Download komprimieren. Reduziert die Downloadgröße."
},
+ "fullExport": {
+ "label": "Mit Metadaten (Experimentell)",
+ "helpText": "Zusätzlich zum Repository Dump werden Metadaten zum Repository und zur SCM-Instanz exportiert. Gespeicherte Passwörter funktionieren nicht bei einem Import in andere SCM-Manager Instanzen. Dieses Feature ist noch experimentell. Es sollte (noch) nicht für Backups genutzt werden!"
+ },
"exportButton": "Repository exportieren",
"exportStarted": "Der Repository Export wurde gestartet. Abhängig von der Größe des Repository kann dies einige Momente dauern."
},
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index b7b6f1b797..1566237066 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -75,6 +75,10 @@
"title": "Dump File",
"helpText": "Select your dump file from which the repository should be imported."
},
+ "fullImport": {
+ "title": "SCM-Manager Repository Archive",
+ "helpText": "Select the archive file which should be imported. The archive must have been exported from an SCM-Manager instance."
+ },
"pending": {
"subtitle": "Importing Repository...",
"infoText": "Your repository is currently being imported. This may take a moment. You will be forwarded as soon as the import is finished. If you leave this page you cannot return to find out the import status."
@@ -88,6 +92,10 @@
"bundle": {
"label": "Import from dump",
"helpText": "The repository will be imported from a dump file."
+ },
+ "fullImport": {
+ "label": "Import from archive with metadata (experimental)",
+ "helpText": "The repository will be imported with metadata. The archive containing the data must be generated by an SCM-Manager instance."
}
}
},
@@ -248,6 +256,10 @@
"label": "Compress",
"helpText": "Compress the export dump size to reduce the download size."
},
+ "fullExport": {
+ "label": "With metadata (Experimental)",
+ "helpText": "In addition to the repository dump, metadata about the repository and SCM instance is exported. Stored passwords will not work if this is imported in other instances of SCM-Manager. This feature is still experimental. Do not use this as a backup mechanism (yet)!"
+ },
"exportButton": "Export Repository",
"exportStarted": "The repository export was started. Depending on the repository size this may take a while."
},
diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx
new file mode 100644
index 0000000000..c75914379b
--- /dev/null
+++ b/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx
@@ -0,0 +1,114 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React, { FC, FormEvent, useState } from "react";
+import NamespaceAndNameFields from "./NamespaceAndNameFields";
+import { File, Repository } from "@scm-manager/ui-types";
+import RepositoryInformationForm from "./RepositoryInformationForm";
+import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
+import { useTranslation } from "react-i18next";
+import { useHistory } from "react-router-dom";
+import ImportFromBundleForm from "./ImportFromBundleForm";
+import ImportFullRepositoryForm from "./ImportFullRepositoryForm";
+
+type Props = {
+ url: string;
+ repositoryType: string;
+ setImportPending: (pending: boolean) => void;
+};
+
+const ImportFullRepository: FC = ({ url, repositoryType, setImportPending }) => {
+ const [repo, setRepo] = useState({
+ name: "",
+ namespace: "",
+ type: repositoryType,
+ contact: "",
+ description: "",
+ _links: {},
+ });
+
+ const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState();
+ const [file, setFile] = useState(null);
+ const history = useHistory();
+ const [t] = useTranslation("repos");
+
+ const handleImportLoading = (loading: boolean) => {
+ setImportPending(loading);
+ setLoading(loading);
+ };
+
+ const isValid = () => Object.values(valid).every((v) => v);
+
+ const submit = (event: FormEvent) => {
+ event.preventDefault();
+ const currentPath = history.location.pathname;
+ setError(undefined);
+ handleImportLoading(true);
+ apiClient
+ .postBinary(url, (formData) => {
+ formData.append("bundle", file, file?.name);
+ formData.append("repository", JSON.stringify(repo));
+ })
+ .then((response) => {
+ const location = response.headers.get("Location");
+ return apiClient.get(location!);
+ })
+ .then((response) => response.json())
+ .then((repo) => {
+ if (history.location.pathname === currentPath) {
+ history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`);
+ }
+ })
+ .catch((error) => {
+ setError(error);
+ handleImportLoading(false);
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default ImportFullRepository;
diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx
new file mode 100644
index 0000000000..5a3911e1db
--- /dev/null
+++ b/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx
@@ -0,0 +1,53 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { FC } from "react";
+import { FileUpload, LabelWithHelpIcon, Checkbox } from "@scm-manager/ui-components";
+import { File } from "@scm-manager/ui-types";
+import { useTranslation } from "react-i18next";
+
+type Props = {
+ setFile: (file: File) => void;
+ setValid: (valid: boolean) => void;
+};
+
+const ImportFullRepositoryForm: FC = ({ setFile, setValid}) => {
+ const [t] = useTranslation("repos");
+
+ return (
+