Skip to content

Commit

Permalink
#105 Set up target percentage of free space in Google Drive and monit…
Browse files Browse the repository at this point in the history
…or it; dependency upgrades; minor fixes
  • Loading branch information
ylexus committed Jun 10, 2021
1 parent 402876b commit 67b6eef
Show file tree
Hide file tree
Showing 34 changed files with 956 additions and 240 deletions.
50 changes: 24 additions & 26 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import java.nio.file.Paths
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.beryx.runtime' version '1.8.5'
id "name.remal.check-updates" version "1.0.211"
id 'org.beryx.runtime' version '1.12.5'
id "name.remal.check-updates" version "1.3.1"

// TODO no good alternative to maven duplicate finder
// id "net.idlestate.gradle-duplicate-classes-check" version "1.0.2"

id "ca.cutterslade.analyze" version "1.4.2"
id "ca.cutterslade.analyze" version "1.6.0"
}

version = readEnvOrDefault 'VERSION', '0.DEV'
Expand Down Expand Up @@ -48,29 +48,25 @@ repositories {
}

dependencies {
ext.orgJunitJupiterVersion = '5.6.0'
ext.orgMockitoVersion = '3.2.4'
ext.orgImmutablesVersion = '2.8.3'
ext.netYudichevJiottyVersion = '1.7.1'
ext.orgJunitJupiterVersion = '5.7.2'
ext.orgMockitoVersion = '3.10.0'
ext.orgImmutablesVersion = '2.8.8'
ext.netYudichevJiottyVersion = '2.0-SNAPSHOT'
ext.orgApacheLoggingLog4jVersion = '2.14.1'

annotationProcessor "org.immutables:value:$orgImmutablesVersion"
testAnnotationProcessor "org.immutables:value:$orgImmutablesVersion"

implementation platform("net.yudichev.jiotty:jiotty-bom:$netYudichevJiottyVersion")
implementation platform("com.google.inject:guice-bom:4.2.2")

// This is needed as we depend on no_aop guice instead (regular Guice does not work on Java 14)
def withoutGuice = {
exclude group: "com.google.inject", module: "guice"
}

implementation "net.yudichev.jiotty:jiotty-common", withoutGuice
implementation "net.yudichev.jiotty:jiotty-connector-google-common", withoutGuice
implementation "net.yudichev.jiotty:jiotty-connector-google-photos", withoutGuice
implementation "com.google.inject.extensions:guice-assistedinject", withoutGuice
implementation "net.yudichev.jiotty:jiotty-common"
implementation "net.yudichev.jiotty:jiotty-connector-google-common"
implementation "net.yudichev.jiotty:jiotty-connector-google-photos"
implementation "net.yudichev.jiotty:jiotty-connector-google-drive"
implementation "com.google.inject.extensions:guice-assistedinject"
implementation "org.slf4j:slf4j-api"
implementation "org.apache.logging.log4j:log4j-api"
implementation "com.google.inject:guice::no_aop"
implementation "com.google.inject:guice"
implementation "javax.inject:javax.inject"
implementation "com.google.guava:guava:29.0-jre"
implementation "com.google.code.findbugs:jsr305"
Expand All @@ -83,23 +79,24 @@ dependencies {
implementation "de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7"
implementation "io.grpc:grpc-api"
implementation "com.squareup.okhttp3:okhttp"
implementation "com.sandec:mdfx:0.1.8"
implementation "com.sandec:mdfx:0.2.1"
implementation 'com.h2database:h2:1.4.200'

// This is to test dependency analyser
//implementation "org.apache.commons:commons-skin:4.2"
compileOnly "org.immutables:value:$orgImmutablesVersion"
runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:2.12.1"
runtimeOnly "org.apache.logging.log4j:log4j-jcl:2.12.1"
runtimeOnly "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.9"
runtimeOnly "com.lmax:disruptor:3.4.2"
runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$orgApacheLoggingLog4jVersion"
runtimeOnly "org.apache.logging.log4j:log4j-jcl:$orgApacheLoggingLog4jVersion"
runtimeOnly "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3"
runtimeOnly "com.lmax:disruptor:3.4.4"

testImplementation "net.yudichev.jiotty:jiotty-common"
testImplementation "org.junit.jupiter:junit-jupiter-api:$orgJunitJupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$orgJunitJupiterVersion"
testImplementation "net.yudichev.jiotty:jiotty-connector-google-drive::tests"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$orgJunitJupiterVersion"
testImplementation "org.mockito:mockito-junit-jupiter:$orgMockitoVersion"
testImplementation "org.hamcrest:hamcrest:2.1"
testImplementation "org.hamcrest:hamcrest:2.2"
testCompileOnly "org.immutables:value:$orgImmutablesVersion"

// these are added by the javafx plugin
Expand Down Expand Up @@ -169,14 +166,14 @@ javafx {
}

runtime {
//noinspection GroovyAssignabilityCheck, GroovyAccessibility
//noinspection GroovyAssignabilityCheck, GroovyAccessibility, GrFinalVariableAccess
options = ['--strip-debug',
'--compress', '2',
'--no-header-files',
'--no-man-pages']

// required by httpclient, missed by default when building and running on Windows
//noinspection GroovyAssignabilityCheck,GroovyAccessibility
//noinspection GroovyAssignabilityCheck,GroovyAccessibility,GrFinalVariableAccess
modules = [
// required by httpclient, missed by default when building and running on Windows
'java.naming',
Expand All @@ -185,6 +182,7 @@ runtime {
// required for a heap dump via JMX
'jdk.management'
]
//noinspection GroovyAssignabilityCheck,GroovyAccessibility,GrFinalVariableAccess
additive = true

jpackage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static net.yudichev.jiotty.common.lang.CompletableFutures.logErrorOnFailure;

final class
CliStarter extends BaseLifecycleComponent {
final class CliStarter extends BaseLifecycleComponent {
private static final Logger logger = LoggerFactory.getLogger(CliStarter.class);
private final Path rootDir;
private final Uploader uploader;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ final class LoggingProgressStatusFactory implements ProgressStatusFactory {
}

@Override
public ProgressStatus create(String name, Optional<Integer> totalCount) {
public ProgressStatus create(String name, @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") Optional<Integer> totalCountOpt) {
return new ProgressStatus() {
private final Lock lock = new ReentrantLock();
private int successCount;
private Optional<Integer> totalCount = totalCountOpt;
private int failureCount;

@Override
Expand All @@ -42,6 +43,12 @@ public void updateSuccess(int newValue) {
throttledLog();
}

@Override
public void updateTotal(int newValue) {
inLock(lock, () -> { totalCount = Optional.of(newValue);});
throttledLog();
}

@Override
public void updateDescription(String newValue) {
// do nothing in CLI - log is enough
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public boolean useCustomCredentials() {
return false;
}

public abstract Optional<FailOnDriveSpaceOption> failOnDriveSpace();

@Value.Check
void validateRelevantDirDepthLimit() {
relevantDirDepthLimit().ifPresent(value -> checkArgument(value > 0, "validateRelevantDirDepthLimit cannot be <=0: %s", value));
Expand Down Expand Up @@ -147,4 +149,17 @@ private static Set<PathMatcher> compile(Set<String> strings) {
.map(fileSystem::getPathMatcher)
.collect(toImmutableSet());
}

@Immutable
@PublicImmutablesStyle
interface BaseFailOnDriveSpaceOption {
Optional<Integer> minFreeMegabytes();

Optional<Double> maxUsedPercentage();

@Value.Check
default void validate() {
checkArgument(minFreeMegabytes().isPresent() ^ maxUsedPercentage().isPresent());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import net.yudichev.jiotty.common.lang.TypedBuilder;
import net.yudichev.jiotty.common.time.TimeModule;
import net.yudichev.jiotty.connector.google.common.GoogleApiAuthSettings;
import net.yudichev.jiotty.connector.google.drive.GoogleDriveModule;
import net.yudichev.jiotty.connector.google.photos.GooglePhotosModule;

import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.function.Consumer;

import static com.google.api.services.drive.DriveScopes.DRIVE_APPDATA;
import static com.google.common.base.Preconditions.checkNotNull;
import static net.yudichev.googlephotosupload.core.AppGlobals.APP_TITLE;
import static net.yudichev.jiotty.common.inject.BindingSpec.providedBy;
Expand Down Expand Up @@ -46,8 +48,13 @@ protected void configure() {
.setApplicationName(APP_TITLE)
.setCredentialsUrl(providedBy(CustomCredentialsManagerImpl.class));
googleApiSettingsCustomiser.accept(googleApiSettingsBuilder);
var settings = googleApiSettingsBuilder.build();
install(GooglePhotosModule.builder()
.setSettings(googleApiSettingsBuilder.build())
.setSettings(settings)
.build());
install(GoogleDriveModule.builder()
.setSettings(settings)
.addScopes(DRIVE_APPDATA)
.build());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.yudichev.googlephotosupload.core;

import java.util.List;
import java.util.concurrent.CompletableFuture;

interface DriveSpaceTracker {
CompletableFuture<Void> reset();

boolean validationEnabled();

void beforeUpload();

void afterUpload(List<PathState> pathStates);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package net.yudichev.googlephotosupload.core;

import com.google.common.collect.ImmutableSet;
import net.yudichev.jiotty.connector.google.drive.GoogleDriveClient;
import net.yudichev.jiotty.connector.google.drive.GoogleDrivePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.concurrent.CompletableFuture.supplyAsync;
import static net.yudichev.jiotty.common.lang.Locks.inLock;
import static net.yudichev.jiotty.common.lang.MoreThrowables.getAsUnchecked;

final class DriveSpaceTrackerImpl implements DriveSpaceTracker {
private static final Logger logger = LoggerFactory.getLogger(DriveSpaceTrackerImpl.class);

private static final long CHECK_SPACE_EVERY_BYTES = 50 * 1024 * 1024; // 50 MB
private static final ImmutableSet<String> FIELDS = ImmutableSet.of("storageQuota/limit", "storageQuota/usage");
private static final byte[] NO_DATA = new byte[0];
private final ProgressStatusFactory progressStatusFactory;
private final GoogleDriveClient googleDriveClient;
private final PreferencesManager preferencesManager;
private final ResourceBundle resourceBundle;
private final CloudOperationHelper cloudOperationHelper;
private final Lock lock = new ReentrantLock();

private ProgressStatus driveSpaceStatus;
private Optional<Long> limit;
private long usage;
private long bytesUploaded;
private long bytesUploadedSinceSpaceCheck;

@Inject
DriveSpaceTrackerImpl(ProgressStatusFactory progressStatusFactory,
GoogleDriveClient googleDriveClient,
PreferencesManager preferencesManager,
ResourceBundle resourceBundle,
CloudOperationHelper cloudOperationHelper) {
this.progressStatusFactory = progressStatusFactory;
this.googleDriveClient = googleDriveClient;
this.preferencesManager = checkNotNull(preferencesManager);
this.resourceBundle = checkNotNull(resourceBundle);
this.cloudOperationHelper = checkNotNull(cloudOperationHelper);
}

@Override
public CompletableFuture<Void> reset() {
return supplyAsync(() -> {
inLock(lock, () -> {
//noinspection AssignmentToNull assigned in the method called next
driveSpaceStatus = null;
refreshDriveQuota();
});
return null;
});
}

@Override
public boolean validationEnabled() {
return inLock(lock, () -> preferencesManager.get().failOnDriveSpace().isPresent() && limit.isPresent());
}

@Override
public void beforeUpload() {
inLock(lock, () -> {
if (driveSpaceStatus != null) {
validateUsage();
}
});
}

@Override
public void afterUpload(List<PathState> pathStates) {
inLock(lock, () -> {
var totalFileSize = pathStates.stream().map(PathState::path).mapToLong(path -> getAsUnchecked(() -> Files.size(path))).sum();
bytesUploaded += totalFileSize;
refreshStatusDescription();
bytesUploadedSinceSpaceCheck += totalFileSize;
if (bytesUploadedSinceSpaceCheck > CHECK_SPACE_EVERY_BYTES) {
logger.debug("bytesUploadedSinceSpaceCheck ({}) > CHECK_SPACE_EVERY_BYTES ({})", bytesUploadedSinceSpaceCheck, CHECK_SPACE_EVERY_BYTES);
bytesUploadedSinceSpaceCheck = 0;
refreshDriveQuota();
validateUsage();
}
});
}

private void refreshStatusDescription() {
if (driveSpaceStatus != null) {
driveSpaceStatus.updateDescription(
String.format(resourceBundle.getString("driveSpaceUploadedTotal"),
limit.map(DriveSpaceTrackerImpl::formatSize).orElse("∞"),
formatSize(bytesUploaded)));
}
}

private void refreshDriveQuota() {
logger.debug("Refreshing drive quota");
if (driveSpaceStatus == null) {
driveSpaceStatus = progressStatusFactory.create(resourceBundle.getString("driveSpaceStatusTitle"), Optional.empty());
}
cloudOperationHelper.withBackOffAndRetry("Get drive quota",
// Creating a file in Drive refreshes usage stats
() -> googleDriveClient.getAppDataFolder(directExecutor()).createFile("file.txt", "text/plain", NO_DATA)
.thenCompose(GoogleDrivePath::delete)
.thenCompose(ignored -> googleDriveClient.aboutDrive(FIELDS, directExecutor())),
value -> {})
.thenAccept(about -> {
limit = Optional.ofNullable(about.getStorageQuota().getLimit());
var usage = about.getStorageQuota().getUsage();
if (usage != null) {
limit.map(DriveSpaceTrackerImpl::toMegabytes).ifPresent(newValue -> driveSpaceStatus.updateTotal(newValue.intValue()));
this.usage = usage;
//noinspection NumericCastThatLosesPrecision
driveSpaceStatus.updateSuccess((int) toMegabytes(usage));
refreshStatusDescription();
}
});
}

private void validateUsage() {
preferencesManager.get().failOnDriveSpace().ifPresent(option -> limit.ifPresent(limitBytes -> option.minFreeMegabytes().ifPresentOrElse(
minFreeMegabytes -> {
if (toMegabytes(limitBytes - usage) <= minFreeMegabytes) {
throw new IllegalStateException(String.format(resourceBundle.getString("driveSpaceMinFreeSpaceViolated"), minFreeMegabytes));
}
},
() -> {
//noinspection OptionalGetWithoutIsPresent mutually exclusive
if (toMegabytes(usage / limitBytes * 100) >= option.maxUsedPercentage().get()) {
throw new IllegalStateException(String
.format(resourceBundle.getString("driveSpaceMaxUsedPercentageViolated"), option.maxUsedPercentage().get()));
}
}
)));
}

private static String formatSize(long bytes) {
if (bytes >= 1024 * 1024) {
return String.format("%,.2f MB", toMegabytes(bytes));
} else if (bytes >= 1024) {
return String.format("%,.2f KB", toKilobytes(bytes));
} else {
return String.format("%s B", bytes);
}
}

private static double toMegabytes(long bytes) {
return toKilobytes(bytes) / 1024;
}

private static double toKilobytes(long bytes) {
return (double) bytes / 1024;
}
}
Loading

0 comments on commit 67b6eef

Please sign in to comment.