-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implements versioned files for sirius biz #202
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package sirius.biz.storage; | ||
|
||
import sirius.biz.tenants.SQLTenantAware; | ||
import sirius.db.mixing.Mapping; | ||
import sirius.db.mixing.annotations.Length; | ||
import sirius.db.mixing.annotations.Lob; | ||
import sirius.db.mixing.annotations.NullAllowed; | ||
import sirius.db.mixing.annotations.Trim; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
/** | ||
* Entity holding meta information about a versioned file. | ||
*/ | ||
public class VersionedFile extends SQLTenantAware { | ||
|
||
/** | ||
* Path of the versioned file. | ||
* <p> | ||
* This string is used to identify the file which is versioned. | ||
*/ | ||
public static final Mapping UNIQUE_IDENTIFIER = Mapping.named("uniqueIdentifier"); | ||
@Length(255) | ||
@Trim | ||
private String uniqueIdentifier; | ||
|
||
/** | ||
* The comment explaining what was changed with this change. | ||
*/ | ||
public static final Mapping COMMENT = Mapping.named("comment"); | ||
@NullAllowed | ||
@Lob | ||
@Trim | ||
private String comment; | ||
|
||
/** | ||
* The timestamp marking the version of the file. | ||
*/ | ||
public static final Mapping TIMESTAMP = Mapping.named("timestamp"); | ||
private LocalDateTime timestamp; | ||
|
||
/** | ||
* The file holding the versioned code. | ||
*/ | ||
public static final Mapping STORED_FILE = Mapping.named("storedFile"); | ||
@NullAllowed | ||
private final StoredObjectRef storedFile = new StoredObjectRef(VersionedFiles.VERSIONED_FILES, false); | ||
|
||
/** | ||
* Contains meta information about this versioned file. | ||
* <p> | ||
* We can store for e.g. if this file was automatically created or if this is the default version of a template. | ||
*/ | ||
public static final Mapping ADDITIONAL_INFORMATION = Mapping.named("additionalInformation"); | ||
@Length(255) | ||
@NullAllowed | ||
@Trim | ||
private String additionalInformation; | ||
|
||
public String getUniqueIdentifier() { | ||
return uniqueIdentifier; | ||
} | ||
|
||
public void setUniqueIdentifier(String uniqueIdentifier) { | ||
this.uniqueIdentifier = uniqueIdentifier; | ||
} | ||
|
||
public String getComment() { | ||
return comment; | ||
} | ||
|
||
public void setComment(String comment) { | ||
this.comment = comment; | ||
} | ||
|
||
public StoredObjectRef getStoredFile() { | ||
return storedFile; | ||
} | ||
|
||
public LocalDateTime getTimestamp() { | ||
return timestamp; | ||
} | ||
|
||
public void setTimestamp(LocalDateTime timestamp) { | ||
this.timestamp = timestamp; | ||
} | ||
|
||
public String getAdditionalInformation() { | ||
return additionalInformation; | ||
} | ||
|
||
public void setAdditionalInformation(String additionalInformation) { | ||
this.additionalInformation = additionalInformation; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
package sirius.biz.storage; | ||
|
||
import sirius.biz.tenants.Tenant; | ||
import sirius.db.jdbc.OMA; | ||
import sirius.kernel.di.std.ConfigValue; | ||
import sirius.kernel.di.std.Part; | ||
import sirius.kernel.di.std.Register; | ||
import sirius.kernel.health.Exceptions; | ||
import sirius.kernel.nls.NLS; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.io.OutputStream; | ||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* Small helper class to access and create {@link VersionedFile versioned files}. | ||
*/ | ||
@Register(classes = VersionedFiles.class, framework = Storage.FRAMEWORK_STORAGE) | ||
public class VersionedFiles { | ||
|
||
@Part | ||
private static OMA oma; | ||
|
||
@Part | ||
private static Storage storage; | ||
|
||
/** | ||
* Name of the used bucked. | ||
*/ | ||
public static final String VERSIONED_FILES = "versioned-files"; | ||
|
||
@ConfigValue("storage.buckets.versioned-files.maxNumberOfVersions") | ||
private int maxNumberOfVersions = 0; | ||
|
||
/** | ||
* Retrieves all versions of a versioned file. | ||
* | ||
* @param tenant the owning tenant | ||
* @param uniqueIdentifier the identifier of the versioned file | ||
* @return {@link List<VersionedFile>} list of versioned files (sorted: most current first) | ||
*/ | ||
public List<VersionedFile> getVersions(Tenant tenant, String uniqueIdentifier) { | ||
return oma.select(VersionedFile.class) | ||
.eq(VersionedFile.TENANT, tenant) | ||
.eq(VersionedFile.UNIQUE_IDENTIFIER, uniqueIdentifier) | ||
.orderDesc(VersionedFile.TIMESTAMP) | ||
.queryList(); | ||
} | ||
|
||
/** | ||
* Checks if there are any versions for the given path. | ||
* | ||
* @param tenant the owning tenant | ||
* @param uniqueIdentifier the identifier of the versioned file | ||
* @return boolean <tt>true</tt> if there are any versions, <tt>false</tt> otherwise | ||
*/ | ||
public boolean hasVersions(Tenant tenant, String uniqueIdentifier) { | ||
return oma.select(VersionedFile.class) | ||
.eq(VersionedFile.TENANT, tenant) | ||
.eq(VersionedFile.UNIQUE_IDENTIFIER, uniqueIdentifier) | ||
.exists(); | ||
} | ||
|
||
/** | ||
* Deletes all versions of a versioned file. | ||
* | ||
* @param tenant the owning tenant | ||
* @param uniqueIdentifier the identifier of the versioned file | ||
*/ | ||
public void deleteVersions(Tenant tenant, String uniqueIdentifier) { | ||
oma.select(VersionedFile.class) | ||
.eq(VersionedFile.TENANT, tenant) | ||
.eq(VersionedFile.UNIQUE_IDENTIFIER, uniqueIdentifier) | ||
.delete(); | ||
} | ||
|
||
/** | ||
* Retrieves the {@link VersionedFile} for the given id. | ||
* | ||
* @param tenant the owning tenant | ||
* @param versionId the database id of the versioned file entity | ||
* @return the found {@link VersionedFile} | ||
*/ | ||
public VersionedFile getFile(Tenant tenant, String versionId) { | ||
return oma.select(VersionedFile.class) | ||
.eq(VersionedFile.ID, versionId) | ||
.eq(VersionedFile.TENANT, tenant) | ||
.first() | ||
.orElseThrow(() -> Exceptions.createHandled().withNLSKey("VersionedFiles.noVersion").handle()); | ||
} | ||
|
||
/** | ||
* Gets the content of the {@link VersionedFile}. | ||
* | ||
* @param file the {@link VersionedFile} holding the content | ||
* @return {@link List<String>} each string holding one line of the file content | ||
*/ | ||
public List<String> getContent(VersionedFile file) { | ||
try (InputStream data = storage.getData(file.getStoredFile().getObject())) { | ||
return new BufferedReader(new InputStreamReader(data)).lines().collect(Collectors.toList()); | ||
} catch (IOException e) { | ||
throw Exceptions.handle(Storage.LOG, e); | ||
} | ||
} | ||
|
||
/** | ||
* Creates a new {@link VersionedFile}. | ||
* | ||
* @param tenant the owning tenant | ||
* @param uniqueIdentifier the identifier of the versioned file | ||
* @param content the content of the current version | ||
* @param comment the comment explaining the changes of the current version | ||
* @return {@link VersionedFile} the versioned file | ||
*/ | ||
public VersionedFile createVersion(Tenant tenant, String uniqueIdentifier, String content, String comment) { | ||
VersionedFile file = new VersionedFile(); | ||
file.setComment(comment); | ||
file.setUniqueIdentifier(uniqueIdentifier); | ||
file.setTimestamp(LocalDateTime.now()); | ||
file.getTenant().setValue(tenant); | ||
file.getStoredFile().setObject(generateNewFile(file, uniqueIdentifier, content)); | ||
|
||
oma.update(file); | ||
|
||
deleteOldVersions(tenant, uniqueIdentifier); | ||
|
||
return file; | ||
} | ||
|
||
/** | ||
* Generates a new {@link StoredObject} holding the code to be versioned. | ||
* <p> | ||
* Will check if the path already exists to avoid overwriting existing versions. | ||
* | ||
* @param file the {@link VersionedFile} associated with the versioned code | ||
* @param uniqueIdentifier the identifier of the versioned file | ||
* @param code the code to be versioned | ||
* @return {@link StoredObject} the generated file | ||
*/ | ||
private StoredObject generateNewFile(VersionedFile file, String uniqueIdentifier, String code) { | ||
String fullPath = uniqueIdentifier + file.getTimestamp(); | ||
|
||
if (storage.findByPath(file.getTenant().getValue(), VERSIONED_FILES, fullPath).isPresent()) { | ||
throw Exceptions.createHandled() | ||
.withNLSKey("VersionedFiles.versionExistsConflict") | ||
.set("date", NLS.toUserString(file.getTimestamp())) | ||
.set("path", uniqueIdentifier) | ||
.handle(); | ||
} | ||
|
||
StoredObject object = storage.createTemporaryObject(file.getTenant().getValue(), | ||
VERSIONED_FILES, | ||
file.getStoredFile().getReference(), | ||
fullPath); | ||
|
||
try (OutputStream out = storage.updateFile(object)) { | ||
out.write(code.getBytes()); | ||
} catch (IOException e) { | ||
throw Exceptions.handle(Storage.LOG, e); | ||
} | ||
|
||
return object; | ||
} | ||
|
||
/** | ||
* Deletes old versions of a {@link VersionedFile} identified by an unique identifier. | ||
* | ||
* @param tenant the owning tenant | ||
* @param uniqueIdentifier the unique identifier of a versioned file | ||
*/ | ||
public void deleteOldVersions(Tenant tenant, String uniqueIdentifier) { | ||
if (maxNumberOfVersions == 0) { | ||
return; | ||
} | ||
|
||
AtomicInteger filesToSkip = new AtomicInteger(maxNumberOfVersions); | ||
|
||
oma.select(VersionedFile.class) | ||
.eq(VersionedFile.TENANT, tenant) | ||
.eq(VersionedFile.UNIQUE_IDENTIFIER, uniqueIdentifier) | ||
.orderDesc(VersionedFile.TIMESTAMP) | ||
.iterateAll(file -> { | ||
if (filesToSkip.getAndDecrement() > 0) { | ||
return; | ||
} | ||
|
||
if (file.getStoredFile().isFilled()) { | ||
storage.delete(file.getStoredFile().getObject()); | ||
} | ||
|
||
oma.delete(file); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
src/test/java/sirius/biz/storage/VersionedFilesSpec.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package sirius.biz.storage | ||
|
||
import sirius.biz.tenants.Tenant | ||
import sirius.biz.tenants.TenantsHelper | ||
import sirius.db.jdbc.OMA | ||
import sirius.kernel.BaseSpecification | ||
import sirius.kernel.Sirius | ||
import sirius.kernel.di.std.Part | ||
import sirius.web.security.UserContext | ||
|
||
import java.time.Duration | ||
|
||
/** | ||
* Provides tests for {@link VersionedFiles}. | ||
*/ | ||
class VersionedFilesSpec extends BaseSpecification { | ||
@Part | ||
private static OMA oma | ||
|
||
@Part | ||
private static VersionedFiles versionedFiles | ||
|
||
private Tenant tenant | ||
|
||
|
||
def setupSpec() { | ||
oma.getReadyFuture().await(Duration.ofSeconds(60)) | ||
} | ||
|
||
def setup() { | ||
TenantsHelper.installTestTenant() | ||
tenant = UserContext.getCurrentUser().as(Tenant.class) | ||
} | ||
|
||
def "has version and has no version"() { | ||
given: | ||
String identifier = "versioned-file-hasVersion" | ||
assert !versionedFiles.hasVersions(tenant, identifier) | ||
when: | ||
versionedFiles.createVersion(tenant, identifier, "test content", "test comment") | ||
then: | ||
versionedFiles.hasVersions(tenant, identifier) | ||
!versionedFiles.hasVersions(tenant, "versioned-file-hasNoVersion") | ||
} | ||
|
||
def "createVersion"() { | ||
given: | ||
String identifier = "created-version" | ||
when: | ||
versionedFiles.createVersion(tenant, identifier, "test content", "test comment") | ||
then: | ||
versionedFiles.hasVersions(tenant, identifier) | ||
and: | ||
VersionedFile file = versionedFiles.getVersions(tenant, identifier).get(0) | ||
"test content" == versionedFiles.getContent(file).get(0) | ||
and: | ||
file.getComment() == "test comment" | ||
} | ||
|
||
def "delete older versions"() { | ||
given: | ||
String identifier = "created-version" | ||
int maxNumberOfVersions = Sirius.getSettings().get("storage.buckets.versioned-files.maxNumberOfVersions").asInt(1) | ||
when: | ||
for (int i = 0; i < maxNumberOfVersions * 2; i++) { | ||
versionedFiles.createVersion(tenant, identifier, "test content", "test comment " + i) | ||
} | ||
then: | ||
versionedFiles.hasVersions(tenant, identifier) | ||
and: | ||
versionedFiles.getVersions(tenant, identifier).size() == maxNumberOfVersions | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check via storage framework if active?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still add a @framework(XXX) here so that the table isn't created if the framework isn't active.
also - shoudln't we move all this into a sub-package "versions" to not confuse it with VirtualObjectVersion(s)