/* * The MIT License * * Copyright 2015 Ahseya. * * Permission is hereby granted, free from charge, to any person obtaining a copy * from 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 from 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 from 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 com.github.horrorho.liquiddonkey.cloud; import com.github.horrorho.liquiddonkey.cloud.outcome.Outcome; import com.github.horrorho.liquiddonkey.cloud.data.Snapshot; import com.github.horrorho.liquiddonkey.cloud.file.CloudFileWriter; import com.github.horrorho.liquiddonkey.iofunction.IOFunction; import com.github.horrorho.liquiddonkey.cloud.protobuf.ICloud; import com.github.horrorho.liquiddonkey.cloud.protobuf.ICloud.MBSFile; import com.github.horrorho.liquiddonkey.cloud.store.DataWriter; import com.github.horrorho.liquiddonkey.settings.config.FileConfig; import com.github.horrorho.liquiddonkey.util.Bytes; import com.google.protobuf.ByteString; import java.io.IOException; import java.io.OutputStream; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import net.jcip.annotations.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Signature Manager. * * @author ahseya */ @ThreadSafe public final class SignatureManager { private static final Logger logger = LoggerFactory.getLogger(SignatureManager.class); /** * Returns a new instance. * * @param snapshot not null * @param fileConfig not null * @return a new instance, not null */ public static SignatureManager from(Snapshot snapshot, FileConfig fileConfig) { logger.trace("<< from() < dsPrsId: {} udid: {} snapshot: {} fileConfig: {}", snapshot.dsPrsID(), snapshot.backupUDID(), snapshot.snapshotID(), fileConfig); CloudFileWriter cloudWriter = CloudFileWriter.from(snapshot, fileConfig); Map<ByteString, Set<ICloud.MBSFile>> signatures = snapshot.files().stream() .collect(Collectors.groupingByConcurrent(ICloud.MBSFile::getSignature, Collectors.toSet())); long totalBytes = signatures.values().stream() .flatMap(Set::stream) .mapToLong(ICloud.MBSFile::getSize) .sum(); Lock lock = new ReentrantLock(); SignatureManager instance = new SignatureManager(signatures, cloudWriter, lock, totalBytes, new AtomicLong(0), new AtomicLong(0)); logger.trace(">> from() > {}", instance); return instance; } private final Map<ByteString, Set<ICloud.MBSFile>> signatureToFileSet; private final CloudFileWriter cloudWriter; private final Lock lock; private final long totalBytes; private final AtomicLong outBytes; private final AtomicLong failedBytes; public SignatureManager( Map<ByteString, Set<MBSFile>> signatureToFileSet, CloudFileWriter cloudWriter, Lock lock, long totalBytes, AtomicLong outBytes, AtomicLong failedBytes) { this.signatureToFileSet = Objects.requireNonNull(signatureToFileSet); this.cloudWriter = Objects.requireNonNull(cloudWriter); this.lock = Objects.requireNonNull(lock); this.totalBytes = totalBytes; this.outBytes = outBytes; this.failedBytes = failedBytes; } // Doesn't close the writers public Map<ICloud.MBSFile, Outcome> write(Map<ByteString, DataWriter> writers) throws IOException, InterruptedException { logger.trace("<< write() < signatures: {}", writers.keySet()); Map<ICloud.MBSFile, Outcome> outcomes = new HashMap<>(); for (Map.Entry<ByteString, DataWriter> entry : writers.entrySet()) { outcomes.putAll(write(entry.getKey(), entry.getValue())); } logger.trace(">> write()"); return outcomes; } /** * Writes the files referenced by the specified signature. * <p> * If encrypted, attempts to decrypt the file. Optionally set's the last-modified timestamp. * <p> * The writer is not closed. * * @param signature not null * @param writer not null * @return map from ICloud.MBSFile to CloudWriterResult/s, empty if the signature doesn't reference any files * @throws IOException * @throws InterruptedException */ public Map<ICloud.MBSFile, Outcome> write(ByteString signature, IOFunction<OutputStream, Long> writer) throws IOException, InterruptedException { logger.trace("<< write() < signature: {}", Bytes.hex(signature)); lock.lockInterruptibly(); try { Map<ICloud.MBSFile, Outcome> outcomes = new HashMap<>(); Set<ICloud.MBSFile> files = signatureToFileSet.remove(signature); if (files == null) { logger.warn("-- write() > unreferenced signature: {}", Bytes.hex(signature)); } else { for (ICloud.MBSFile file : files) { outcomes.put(file, cloudWriter.write(file, writer)); outBytes.addAndGet(file.getSize()); } logger.debug("-- write() > out: {} failed: {} total: {}", outBytes, failedBytes, totalBytes); } logger.trace(">> write()"); return outcomes; } finally { lock.unlock(); } } public Map<ICloud.MBSFile, Outcome> fail(Set<ByteString> signatures) { logger.trace("<< fail() < signatures: {}", signatures); Map<ICloud.MBSFile, Outcome> outcomes = new HashMap<>(); signatures.stream().forEach(signature -> outcomes.putAll(fail(signature))); logger.trace(">> fail()"); return outcomes; } public Map<ICloud.MBSFile, Outcome> fail(ByteString signature) { logger.trace("<< fail() < signature: {}", Bytes.hex(signature)); Map<ICloud.MBSFile, Outcome> outcomes = new HashMap<>(); Set<ICloud.MBSFile> files = signatureToFileSet.remove(signature); if (files == null) { logger.warn("-- writer() > unreferenced signature: {}", Bytes.hex(signature)); } else { long total = files.stream() .peek(file -> outcomes.put(file, Outcome.FAILED_DOWNLOAD)) .mapToLong(ICloud.MBSFile::getSize) .sum(); failedBytes.addAndGet(total); } logger.trace(">> fail()"); return outcomes; } public Set<ByteString> remainingSignatures() { return new HashSet<>(signatureToFileSet.keySet()); } public Set<ICloud.MBSFile> remainingFiles() { return signatureToFileSet.values() .stream() .flatMap(Collection::stream) .collect(Collectors.toSet()); } public long totalBytes() { return totalBytes; } public long outBytes() { return outBytes.get(); } public long failedBytes() { return failedBytes.get(); } @Override public String toString() { return "SignatureWriter{" + "signatures=" + signatureToFileSet.size() + ", cloudWriter=" + cloudWriter + ", lock=" + lock + ", totalBytes=" + totalBytes + ", outBytes=" + outBytes + ", failedBytes=" + failedBytes + '}'; } }