Skip to content
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 4 commits into from
Nov 12, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/main/java/sirius/biz/storage/VersionedFile.java
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 {
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

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)


/**
* 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;
}
}
200 changes: 200 additions & 0 deletions src/main/java/sirius/biz/storage/VersionedFiles.java
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);
});
}
}
2 changes: 2 additions & 0 deletions src/main/resources/biz_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ UserAccountController.forgotPassword.reason = Die "Passwort-Vergessen" Funktion
UserAccountController.logout = Abmelden
UserAccountController.noUserFoundForEmail = Für die angegebene eMail-Adresse wurde kein Benutzer gefunden.
UserAccountController.tooManyUsersFoundForEmail = Die angegebene eMail-Adresse ist nicht eindeutig.
VersionedFiles.noVersion = Keine Version gefunden
VersionedFiles.versionExistsConflict = Die Version ${date} für den Pfad ${uniqueIdentifier} existiert bereits und kann deswegen nicht verwendet werden.
VirtualObject.download = Herunterladen
VirtualObject.fileSize = Größe
VirtualObject.physicalKey = Physikalischer Schlüssel
Expand Down
10 changes: 10 additions & 0 deletions src/main/resources/component-biz.conf
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ storage {
deleteFilesAfterDays = 30
}

# Defines storage for versioned files
versioned-files {
canCreate = false
canEdit = false
canDelete = false

# number of versions kept from one versioned file
# setting this number to 0 will keep all versions
maxNumberOfVersions = 0
}
}

}
Expand Down
73 changes: 73 additions & 0 deletions src/test/java/sirius/biz/storage/VersionedFilesSpec.groovy
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
}
}
Loading