diff --git a/pom.xml b/pom.xml index 3e2b0ea..d9e89d3 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,11 @@ commons-io 2.6 + + commons-codec + commons-codec + 1.14 + com.formdev flatlaf @@ -47,15 +52,14 @@ 0.5.4 - io.takari.zsync - zsync-parent - 0.1.0 - pom + com.github.mfornos + humanize-slim + 1.2.2 - com.github.bitshifted + com.github.Gurkengewuerz zsyncer - -f69d844481-1 + locale_fix-2f7565d392-1 com.squareup.okhttp3 diff --git a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form index 07ce0d0..d061ed7 100644 --- a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form +++ b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form @@ -3,7 +3,7 @@ - + @@ -407,7 +407,7 @@ - + @@ -423,7 +423,7 @@ - + @@ -437,7 +437,7 @@ - + @@ -448,7 +448,7 @@ - + @@ -526,6 +526,23 @@ + + + + + + + + + + + + + + + + + @@ -573,22 +590,9 @@ - - - - - - - - - - - - - - + @@ -596,7 +600,7 @@ - + @@ -610,17 +614,9 @@ - + - - - - - - - - - + @@ -628,7 +624,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -671,24 +699,24 @@ - + - + - + - + diff --git a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java index ce730ee..06b2768 100644 --- a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java +++ b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java @@ -5,6 +5,7 @@ import de.mc8051.arma3launcher.model.JCheckBoxTree; import de.mc8051.arma3launcher.model.ModListRenderer; import de.mc8051.arma3launcher.model.PresetListRenderer; import de.mc8051.arma3launcher.model.PresetTableModel; +import de.mc8051.arma3launcher.model.RepositoryTreeNode; import de.mc8051.arma3launcher.model.ServerTableModel; import de.mc8051.arma3launcher.objects.AbstractMod; import de.mc8051.arma3launcher.objects.Mod; @@ -13,9 +14,12 @@ import de.mc8051.arma3launcher.objects.Modset; import de.mc8051.arma3launcher.objects.Server; import de.mc8051.arma3launcher.repo.FileChecker; import de.mc8051.arma3launcher.repo.RepositoryManger; +import de.mc8051.arma3launcher.repo.SyncList; +import de.mc8051.arma3launcher.repo.Syncer; import de.mc8051.arma3launcher.steam.SteamTimer; import de.mc8051.arma3launcher.utils.Callback; import de.mc8051.arma3launcher.utils.LangUtils; +import humanize.Humanize; import javax.swing.*; import javax.swing.event.ListSelectionEvent; @@ -24,6 +28,7 @@ import javax.swing.plaf.basic.BasicTabbedPaneUI; import javax.swing.text.DefaultFormatter; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.ActionEvent; @@ -37,11 +42,6 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; @@ -115,8 +115,8 @@ public class LauncherGUI implements Observer { private JProgressBar syncCheckProgress; private JButton syncCheckAbortButton; private JButton syncCheckButton; - private JProgressBar progressBar2; - private JProgressBar progressBar3; + public JProgressBar syncDownloadProgress; + public JProgressBar syncFileProgress; private JButton syncDownloadButton; private JButton syncDownloadAbortButton; private JButton syncPauseButton; @@ -130,29 +130,33 @@ public class LauncherGUI implements Observer { private JLabel syncAddedFilesLabel; private JLabel syncChangedFilesLabel; private JLabel syncSizeLabel; + private JLabel syncChangedFileSizeLabel; + private JLabel syncFileCountLabel; + public JLabel syncDownloadedLabel; + public JLabel syncDownloadSpeedLabel; private JCheckBoxTree repoTree; private FileChecker fileChecker; - - // TODO: Updater - /* - Prüfung - In eine Liste hinzufügen wenn Datei in modset.json (Neu runterladen), nicht in modset.json (zum Löschen) oder die Größe unterschiedlich ist (Geändert) - Checkboxen beim Syncronisieren deaktivieren - */ + private Syncer syncer; + private SyncList lastSynclist; public LauncherGUI() { fileChecker = new FileChecker(syncCheckProgress); + syncer = new Syncer(this); RepositoryManger.getInstance().addObserver(this); SteamTimer.addObserver(this); fileChecker.addObserver(this); + syncer.addObserver(this); updateTreePanel.remove(tree1); repoTree = new JCheckBoxTree(); updateTreePanel.add(repoTree, BorderLayout.CENTER); + DefaultTreeModel model = (DefaultTreeModel) repoTree.getModel(); + model.setRoot(new RepositoryTreeNode("Repository")); + updateTreePanel.revalidate(); updateTreePanel.repaint(); @@ -221,7 +225,6 @@ public class LauncherGUI implements Observer { if (!e.getValueIsAdjusting()) { PresetTableModel m = (PresetTableModel) presetList.getModel(); Modset modset = (Modset) m.getElementAt(presetList.getSelectedIndex()); - System.out.println(modset.getName()); if (modset.getType() == Modset.Type.SERVER) { renamePresetButton.setEnabled(false); @@ -283,6 +286,11 @@ public class LauncherGUI implements Observer { new Thread(() -> { RepositoryManger.getInstance().refreshMeta(); + try { + Thread.sleep(750); + } catch (InterruptedException e) { + e.printStackTrace(); + } RepositoryManger.getInstance().refreshModset(); }).start(); @@ -294,7 +302,8 @@ public class LauncherGUI implements Observer { syncCheckStatusLabel.setText("Running!"); new Thread(() -> fileChecker.check()).start(); - // TODO: disable JTree Checkboxes + repoTree.setCheckboxesEnabled(false); + repoTree.setCheckboxesChecked(false); } }); @@ -304,6 +313,28 @@ public class LauncherGUI implements Observer { fileChecker.stop(); } }); + + syncDownloadButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + new Thread(() -> syncer.sync(lastSynclist.clone())).start(); + } + }); + + syncDownloadAbortButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + syncer.stop(); + } + }); + + syncPauseButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + syncer.setPaused(!syncer.isPaused()); + syncPauseButton.setEnabled(false); + } + }); } public static void infoBox(String infoMessage, String titleBar) { @@ -440,7 +471,7 @@ public class LauncherGUI implements Observer { } String modPath = ArmA3Launcher.user_config.get("client", "modPath"); - if(sPath.equalsIgnoreCase(modPath)) { + if (sPath.equalsIgnoreCase(modPath)) { SwingUtilities.invokeLater(() -> errorBox(LangUtils.getInstance().getString("same_mod_arma_dir_msg"), LangUtils.getInstance().getString("same_mod_arma_dir"))); return false; } @@ -457,7 +488,7 @@ public class LauncherGUI implements Observer { String sPath = path.getAbsolutePath(); String armaPath = ArmA3Launcher.user_config.get("client", "armaPath"); - if(sPath.equalsIgnoreCase(armaPath)) { + if (sPath.equalsIgnoreCase(armaPath)) { SwingUtilities.invokeLater(() -> errorBox(LangUtils.getInstance().getString("same_mod_arma_dir_msg"), LangUtils.getInstance().getString("same_mod_arma_dir"))); return false; } @@ -550,78 +581,54 @@ public class LauncherGUI implements Observer { spinner.addChangeListener(new SettingsHandler.SpinnerListener(paraObj)); } - public ArrayList getSyncList() { - ArrayList modList = new ArrayList<>(); + public SyncList getSyncList() { + SyncList synclist = new SyncList(); - HashMap> tempMap = new HashMap<>(); - for (TreePath checkedPath : repoTree.getCheckedPaths()) { - DefaultMutableTreeNode tn = (DefaultMutableTreeNode)checkedPath.getLastPathComponent(); + DefaultTreeModel model = (DefaultTreeModel) repoTree.getModel(); + RepositoryTreeNode root = (RepositoryTreeNode) model.getRoot(); + for (TreeNode leaf : root.getAllLeafNodes()) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) leaf; + TreeNode[] path = node.getPath(); + boolean isSelected = repoTree.isSelected(new TreePath(path)); + if (!isSelected) continue; - if(tn.getChildCount() > 0) continue; - Object[] path = checkedPath.getPath(); - DefaultMutableTreeNode[] modifiedArray = Arrays.stream(Arrays.copyOfRange(path, 1, path.length)).toArray(DefaultMutableTreeNode[]::new); - - ArrayList strings = new ArrayList<>(); - if(tempMap.containsKey(String.valueOf(modifiedArray[0].getUserObject()))) { - strings = tempMap.get(String.valueOf(modifiedArray[0].getUserObject())); + ArrayList treePathList = new ArrayList<>(); + for (int i = 2; i < path.length; i++) { + treePathList.add(String.valueOf(((DefaultMutableTreeNode) path[i]).getUserObject())); } + String treePath = String.join("/", treePathList); + String modname = String.valueOf(((DefaultMutableTreeNode) path[1]).getUserObject()); - String modPath = ""; - for (int i = 1; i < modifiedArray.length; i++) { - modPath += String.valueOf(modifiedArray[i].getUserObject()) + "/"; - } - modPath = modPath.isEmpty() ? "" : modPath.substring(0, modPath.length() - 1); - strings.add(modPath); - - tempMap.put((String) modifiedArray[0].getUserObject(), strings); - } - - for (Map.Entry> entry : tempMap.entrySet()) { - String modS = entry.getKey(); - ArrayList modlistS = entry.getValue(); - - if(modlistS.isEmpty()) { - for (AbstractMod abstractMod : RepositoryManger.MOD_LIST) { - if (abstractMod.getName().equals(modS)) { - modList.add(abstractMod); + if (fileChecker.getChanged().containsKey(modname)) { + for (ModFile modFile : fileChecker.getChanged().get(modname)) { + if (String.join("/", modFile.getPath()).equals(treePath)) { + synclist.add(modFile); break; } } - } else { - for (AbstractMod abstractMod : RepositoryManger.MOD_LIST) { - if (abstractMod.getName().equals(modS)) { - if(!(abstractMod instanceof Mod)) continue; - Mod m = ((Mod) abstractMod).clone(); + } - for (int i = 0; i < m.getFiles().size(); i++) { - boolean found = false; - for (String pathS : modlistS) { - if(m.getFiles().get(i).getModfileString().equals(pathS)) { - found = true; - } - } - - if(!found) { - m.getFiles().remove(i); - } - } - - modList.add(m); + if (fileChecker.getAdded().containsKey(modname)) { + for (ModFile modFile : fileChecker.getAdded().get(modname)) { + if (String.join("/", modFile.getPath()).equals(treePath)) { + synclist.add(modFile); + break; } } } } + synclist.setDeleted(fileChecker.getDeleted()); - return modList; + return synclist; } public void updateModList(Modset modset) { ListModel model = (ListModel) modList.getModel(); // TODO: Show All Mods (keyname) - // TODO: Show not installed Mods with red font - // TODO: Select Mod if in modset.Mods - // TODO: Custom Checkbox Render - // TODO: Wenn modset.type == Server alle Checkboxen deaktivieren! + // Show not installed Mods with red font + // Select Mod if in modset.Mods + // Custom Checkbox Render + // Wenn modset.type == Server alle Checkboxen deaktivieren! } public void updateRepoTree() { @@ -629,28 +636,26 @@ public class LauncherGUI implements Observer { collapseAllButton.setEnabled(false); DefaultTreeModel model = (DefaultTreeModel) repoTree.getModel(); - DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot(); - root.setUserObject("Repository"); + RepositoryTreeNode root = (RepositoryTreeNode) model.getRoot(); root.removeAllChildren(); for (AbstractMod abstractMod : RepositoryManger.MOD_LIST) { if (abstractMod instanceof Mod) { // Whole Folder - // TODO: Recursives Ordner Parsen und einzelne Treenodes erstellen Mod m = (Mod) abstractMod; - DefaultMutableTreeNode modFolder = new DefaultMutableTreeNode(m.getName(), true); + RepositoryTreeNode modFolder = new RepositoryTreeNode(m.getName(), true); model.insertNodeInto(modFolder, root, root.getChildCount()); for (ModFile modfile : m.getFiles()) { - DefaultMutableTreeNode lastNode = modFolder; + RepositoryTreeNode lastNode = modFolder; ArrayList path = modfile.getPath(); - for (int i = 0; i < path.size(); i++) { + for (int i = 0; i < path.size() -1; i++) { boolean found = false; for (int j = 0; j < lastNode.getChildCount(); j++) { - DefaultMutableTreeNode temp = (DefaultMutableTreeNode) lastNode.getChildAt(j); + RepositoryTreeNode temp = (RepositoryTreeNode) lastNode.getChildAt(j); if (temp.getUserObject().equals(path.get(i))) { found = true; lastNode = temp; @@ -659,26 +664,25 @@ public class LauncherGUI implements Observer { } if (!found) { - DefaultMutableTreeNode temp = new DefaultMutableTreeNode(path.get(i)); + RepositoryTreeNode temp = new RepositoryTreeNode(path.get(i)); model.insertNodeInto(temp, lastNode, lastNode.getChildCount()); lastNode = temp; } } - - model.insertNodeInto(new DefaultMutableTreeNode(modfile.getName()), lastNode, lastNode.getChildCount()); + model.insertNodeInto(new RepositoryTreeNode(modfile.getName(), getNodeColor(m.getName(), modfile)), lastNode, lastNode.getChildCount()); } sort(modFolder); } else if (abstractMod instanceof ModFile) { // Just a Single FIle ModFile m = (ModFile) abstractMod; - model.insertNodeInto(new DefaultMutableTreeNode(m.getName(), false), root, root.getChildCount()); + model.insertNodeInto(new RepositoryTreeNode(m.getName(), getNodeColor(m.getName(), m), false), root, root.getChildCount()); } } sort(root); + setParentColor(root); repoTree.clearCheckChangeEventListeners(); - repoTree.resetCheckingState(); SwingUtilities.invokeLater(() -> { @@ -690,38 +694,90 @@ public class LauncherGUI implements Observer { updateTreePanel.repaint(); }); + repoTree.addCheckChangeEventListener(new JCheckBoxTree.CheckChangeEventListener() { + @Override + public void checkStateChanged(JCheckBoxTree.CheckChangeEvent event) { + lastSynclist = getSyncList(); + if (lastSynclist.getSize() != 0) + syncSizeLabel.setText(Humanize.binaryPrefix(lastSynclist.getSize())); + else syncSizeLabel.setText("0.0 B"); + if (lastSynclist.getCount() != 0) { + syncDownloadButton.setEnabled(true); + syncFileCountLabel.setText("" + lastSynclist.getCount()); + } else { + syncDownloadButton.setEnabled(false); + syncFileCountLabel.setText(""); + } + } + }); + expandAllButton.setEnabled(true); collapseAllButton.setEnabled(true); } - public DefaultMutableTreeNode sort(DefaultMutableTreeNode node) { + public Color getNodeColor(String mod, ModFile mf) { + if (fileChecker.getAdded().containsKey(mod)) { + ArrayList mfList = fileChecker.getAdded().get(mod); + for (ModFile modFile : mfList) { + if (modFile.getLocaleFile().getPath().equals(mf.getLocaleFile().getPath())) return Color.RED; + } + } + + if (fileChecker.getChanged().containsKey(mod)) { + ArrayList mfList = fileChecker.getChanged().get(mod); + for (ModFile modFile : mfList) { + if (modFile.getLocaleFile().getPath().equals(mf.getLocaleFile().getPath())) return Color.ORANGE; + } + } + + return null; + } + + public void setParentColor(RepositoryTreeNode node) { + for (TreeNode leaf : node.getAllLeafNodes()) { + if (!(leaf instanceof RepositoryTreeNode)) continue; + RepositoryTreeNode mLeaf = (RepositoryTreeNode) leaf; + TreeNode[] path = mLeaf.getPath(); + + if (mLeaf.getLabelColor() == null) continue; + for (int i = 0; i < path.length - 1; i++) { + if (!(path[i] instanceof RepositoryTreeNode)) continue; + RepositoryTreeNode parent = (RepositoryTreeNode) path[i]; + if (parent.getLabelColor() == mLeaf.getLabelColor()) continue; + if (parent.getLabelColor() == Color.RED) continue; + parent.setLabelColor(mLeaf.getLabelColor()); + } + } + } + + public RepositoryTreeNode sort(RepositoryTreeNode node) { //sort alphabetically - for(int i = 0; i < node.getChildCount() - 1; i++) { - DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); + for (int i = 0; i < node.getChildCount() - 1; i++) { + RepositoryTreeNode child = (RepositoryTreeNode) node.getChildAt(i); String nt = child.getUserObject().toString(); - for(int j = i + 1; j <= node.getChildCount() - 1; j++) { - DefaultMutableTreeNode prevNode = (DefaultMutableTreeNode) node.getChildAt(j); + for (int j = i + 1; j <= node.getChildCount() - 1; j++) { + RepositoryTreeNode prevNode = (RepositoryTreeNode) node.getChildAt(j); String np = prevNode.getUserObject().toString(); - if(nt.compareToIgnoreCase(np) > 0) { + if (nt.compareToIgnoreCase(np) > 0) { node.insert(child, j); node.insert(prevNode, i); } } - if(child.getChildCount() > 0) { + if (child.getChildCount() > 0) { sort(child); } } //put folders first - normal on Windows and some flavors of Linux but not on Mac OS X. - for(int i = 0; i < node.getChildCount() - 1; i++) { - DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); - for(int j = i + 1; j <= node.getChildCount() - 1; j++) { - DefaultMutableTreeNode prevNode = (DefaultMutableTreeNode) node.getChildAt(j); + for (int i = 0; i < node.getChildCount() - 1; i++) { + RepositoryTreeNode child = (RepositoryTreeNode) node.getChildAt(i); + for (int j = i + 1; j <= node.getChildCount() - 1; j++) { + RepositoryTreeNode prevNode = (RepositoryTreeNode) node.getChildAt(j); - if(!prevNode.isLeaf() && child.isLeaf()) { + if (!prevNode.isLeaf() && child.isLeaf()) { node.insert(child, j); node.insert(prevNode, i); } @@ -778,30 +834,55 @@ public class LauncherGUI implements Observer { refreshRepoButton.setEnabled(false); break; } - } else if(s.equals("fileChecker")) { + } else if (s.equals("fileChecker")) { syncCheckButton.setEnabled(true); syncCheckAbortButton.setEnabled(false); syncCheckStatusLabel.setText("Finished!"); updateRepoTree(); - // TODO: Label einfärben - // TODO: Enable Tree Checkboxes + + repoTree.setCheckboxesEnabled(true); syncDownloadButton.setEnabled(true); syncAddedFilesLabel.setText(String.valueOf(fileChecker.getAddedCount())); syncChangedFilesLabel.setText(String.valueOf(fileChecker.getChangedCount())); syncDeletedFilesLabel.setText(String.valueOf(fileChecker.getDeletedCount())); - syncSizeLabel.setText(String.valueOf(fileChecker.getSize())); // TODO: Make Humanreadable + syncDownloadAbortButton.setEnabled(false); + syncDownloadButton.setEnabled(true); + syncPauseButton.setEnabled(false); + + syncChangedFileSizeLabel.setText(Humanize.binaryPrefix(fileChecker.getSize())); } else if (s.equals("fileCheckerStopped")) { syncCheckButton.setEnabled(true); syncCheckAbortButton.setEnabled(false); syncCheckProgress.setValue(0); syncCheckStatusLabel.setText("Failed!"); + repoTree.setCheckboxesEnabled(false); + + syncDownloadAbortButton.setEnabled(false); + syncDownloadButton.setEnabled(false); + syncPauseButton.setEnabled(false); + + repoTree.setCheckboxesChecked(false); syncAddedFilesLabel.setText("" + 0); syncChangedFilesLabel.setText("" + 0); syncDeletedFilesLabel.setText("" + 0); - syncSizeLabel.setText("0.0 B"); + syncChangedFileSizeLabel.setText("0.0 B"); + } else if (s.equals("syncStopped")) { + new Thread(() -> fileChecker.check()).start(); + } else if (s.equals("syncComplete")) { + new Thread(() -> fileChecker.check()).start(); + } else if (s.equals("syncContinue")) { + syncDownloadAbortButton.setEnabled(true); + syncPauseButton.setEnabled(true); + syncPauseButton.setText(LangUtils.getInstance().getString("pause")); + syncDownloadButton.setEnabled(false); + } else if (s.equals("syncPaused")) { + syncDownloadAbortButton.setEnabled(true); + syncPauseButton.setEnabled(true); + syncPauseButton.setText(LangUtils.getInstance().getString("resume")); + syncDownloadButton.setEnabled(false); } } } diff --git a/src/main/java/de/mc8051/arma3launcher/model/JCheckBoxTree.java b/src/main/java/de/mc8051/arma3launcher/model/JCheckBoxTree.java index a45eafc..6fd9d19 100644 --- a/src/main/java/de/mc8051/arma3launcher/model/JCheckBoxTree.java +++ b/src/main/java/de/mc8051/arma3launcher/model/JCheckBoxTree.java @@ -17,6 +17,7 @@ import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; +import java.util.Set; /** * Created by SomethingSomething https://stackoverflow.com/a/21851201/5605489 @@ -31,6 +32,7 @@ public class JCheckBoxTree extends JTree { // Defining data structure that will enable to fast check-indicate the state of each node // It totally replaces the "selection" mechanism of the JTree private class CheckedNode { + boolean isEnabled; boolean isSelected; boolean hasChildren; boolean allChildrenSelected; @@ -38,6 +40,7 @@ public class JCheckBoxTree extends JTree { public CheckedNode(boolean isSelected_, boolean hasChildren_, boolean allChildrenSelected_) { isSelected = isSelected_; hasChildren = hasChildren_; + isEnabled = true; allChildrenSelected = allChildrenSelected_; } } @@ -107,6 +110,11 @@ public class JCheckBoxTree extends JTree { return cn.isSelected && cn.hasChildren && !cn.allChildrenSelected; } + public boolean isSelected(TreePath path) { + CheckedNode cn = nodesCheckingState.get(path); + return cn.isSelected; + } + public void resetCheckingState() { nodesCheckingState = new HashMap(); checkedPaths = new HashSet(); @@ -148,12 +156,21 @@ public class JCheckBoxTree extends JTree { boolean hasFocus) { checkBox.setText(value.toString()); DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + + if (node instanceof RepositoryTreeNode && ((RepositoryTreeNode) node).getLabelColor() != null) { + checkBox.setForeground(((RepositoryTreeNode) node).getLabelColor()); + } else { + checkBox.setForeground(UIManager.getColor("CheckBox.foreground")); + } + TreePath tp = new TreePath(node.getPath()); CheckedNode cn = nodesCheckingState.get(tp); if (cn == null) { return this; } + checkBox.setSelected(cn.isSelected); + checkBox.setEnabled(cn.isEnabled); checkBox.setOpaque(cn.isSelected && cn.hasChildren && !cn.allChildrenSelected); return this; } @@ -264,6 +281,26 @@ public class JCheckBoxTree extends JTree { } } + public void setCheckboxesChecked(boolean state) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) getModel().getRoot(); + checkSubTree(new TreePath(node.getPath()), state); + } + + public void setCheckboxesEnabled(TreePath tp, boolean state) { + CheckedNode cn = nodesCheckingState.get(tp); + cn.isEnabled = state; + + DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp.getLastPathComponent(); + for (int i = 0; i < node.getChildCount(); i++) { + setCheckboxesEnabled(tp.pathByAddingChild(node.getChildAt(i)), state); + } + } + + public void setCheckboxesEnabled(boolean state) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) getModel().getRoot(); + setCheckboxesEnabled(new TreePath(node.getPath()), state); + } + public void expandAllNodes() { setTreeExpandedState(true); } @@ -274,13 +311,13 @@ public class JCheckBoxTree extends JTree { private void setTreeExpandedState(boolean expanded) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) getModel().getRoot(); - setNodeExpandedState( node, expanded); + setNodeExpandedState(node, expanded); } private void setNodeExpandedState(DefaultMutableTreeNode node, boolean expanded) { ArrayList list = Collections.list(node.children()); for (TreeNode treeNode : list) { - setNodeExpandedState((DefaultMutableTreeNode)treeNode, expanded); + setNodeExpandedState((DefaultMutableTreeNode) treeNode, expanded); } if (!expanded && node.isRoot()) { return; diff --git a/src/main/java/de/mc8051/arma3launcher/model/RepositoryTreeNode.java b/src/main/java/de/mc8051/arma3launcher/model/RepositoryTreeNode.java new file mode 100644 index 0000000..324e76a --- /dev/null +++ b/src/main/java/de/mc8051/arma3launcher/model/RepositoryTreeNode.java @@ -0,0 +1,54 @@ +package de.mc8051.arma3launcher.model; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import java.awt.*; +import java.util.HashSet; +import java.util.Set; + +/** + * Created by gurkengewuerz.de on 26.03.2020. + */ +public class RepositoryTreeNode extends DefaultMutableTreeNode { + + private Color labelColor = null; + + public RepositoryTreeNode(String userObject, boolean allowChildren) { + super(userObject, allowChildren); + } + + public RepositoryTreeNode(String userObject, Color labelColor, boolean allowChildren) { + super(userObject, allowChildren); + this.labelColor = labelColor; + } + + public RepositoryTreeNode(String userObject, Color labelColor) { + super(userObject); + this.labelColor = labelColor; + } + + public RepositoryTreeNode(String userObject) { + super(userObject); + } + + public Color getLabelColor() { + return labelColor; + } + + public void setLabelColor(Color labelColor) { + this.labelColor = labelColor; + } + + public Set getAllLeafNodes() { + Set leafNodes = new HashSet<>(); + if (this.children == null) { + leafNodes.add(this); + } else { + for (Object child : this.children) { + if (child instanceof RepositoryTreeNode) + leafNodes.addAll(((RepositoryTreeNode) child).getAllLeafNodes()); + } + } + return leafNodes; + } +} diff --git a/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java b/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java index 8d0176c..c5b357c 100644 --- a/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java +++ b/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java @@ -1,10 +1,19 @@ package de.mc8051.arma3launcher.objects; +import de.mc8051.arma3launcher.ArmA3Launcher; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FilenameUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Created by gurkengewuerz.de on 25.03.2020. @@ -17,8 +26,11 @@ public class ModFile implements AbstractMod { private String filename; private String extension; private String modfileString; + private String sha1sum; + private String parent; + private String localGeneratedSHA1sum = ""; - public ModFile(File f, String modfile, long size) { + public ModFile(File f, String modfile, String parent, long size, String sha1sum) { // File: Abosolut Path // modfile: addons/config/something.pbo // size: size as in metafile on server @@ -28,6 +40,12 @@ public class ModFile implements AbstractMod { this.filename = FilenameUtils.getBaseName(modfile); this.extension = FilenameUtils.getExtension(modfile); this.modfileString = modfile; + this.sha1sum = sha1sum.toLowerCase(); + this.parent = parent; + } + + public ModFile(File f, String modfile, long size, String sha1sum) { + this(f, modfile, null, size, sha1sum); } public long getSize() { @@ -48,23 +66,23 @@ public class ModFile implements AbstractMod { public ArrayList getPath() { ArrayList list = new ArrayList<>(); - File relativePath = new File("./"+ modfileString); + File relativePath = new File("./" + modfileString); + do { list.add(relativePath.getName()); relativePath = relativePath.getParentFile(); } while (relativePath.getParentFile() != null); - list.remove(0); Collections.reverse(list); return list; } public long getLocalSize() { - if(!f.exists() || !f.isFile()) return -1; + if (!f.exists() || !f.isFile()) return -1; return f.length(); } public boolean exists() { - if(!f.exists() || !f.isFile()) return false; + if (!f.exists() || !f.isFile()) return false; return true; } @@ -79,4 +97,45 @@ public class ModFile implements AbstractMod { public File getLocaleFile() { return f; } + + public String getSHA1Sum() { + return sha1sum; + } + + public String getLocalGeneratedSHA1Sum() { + try { + if (localGeneratedSHA1sum.isEmpty() && exists()) { + localGeneratedSHA1sum = DigestUtils.sha1Hex(new FileInputStream(f.getAbsolutePath())).toLowerCase(); + } + } catch (IOException e) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); + } + return localGeneratedSHA1sum; + } + + public String getRemoteFile() { + String s = ArmA3Launcher.config.getString("sync.url"); + if (parent == null || parent.isEmpty()) { + return s + "/" + encodeToURL(getName()); + } + + s += "/" + encodeToURL(parent); + for (String seg : getPath()) { + s += "/" + encodeToURL(seg); + } + return s; + } + + public String getParent() { + return parent; + } + + private String encodeToURL(String s) { + try { + return URLEncoder.encode(s, StandardCharsets.UTF_8.name()).replace("+", "%20").replace("@", "%40"); + } catch (UnsupportedEncodingException e) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); + } + return ""; + } } diff --git a/src/main/java/de/mc8051/arma3launcher/repo/DownloadThread.java b/src/main/java/de/mc8051/arma3launcher/repo/DownloadThread.java deleted file mode 100644 index 3163b02..0000000 --- a/src/main/java/de/mc8051/arma3launcher/repo/DownloadThread.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.mc8051.arma3launcher.repo; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Created by gurkengewuerz.de on 24.03.2020. - */ -public class DownloadThread implements Runnable { - - private ProcessBuilder processBuilder; - private Process process; - private Thread thread; - - private Status status = Status.PENDING; - - public DownloadThread(ProcessBuilder processBuilder) { - this.processBuilder = processBuilder; - this.processBuilder.redirectErrorStream(true); - - thread = new Thread(this); - thread.start(); - } - - public void stop() { - process.destroy(); - thread.interrupt(); - } - - public Status getStatus() { - return status; - } - - @Override - public void run() { - try { - process = processBuilder.start(); - status = Status.RUNNING; - - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line; - while ( (line = reader.readLine()) != null && !thread.isInterrupted()) { - System.out.println(line); - } - - int exitVal = process.waitFor(); - if(exitVal == 0) status = Status.FINNISHED; - else status = Status.ERROR; - - System.out.println(exitVal); - } catch (IOException | InterruptedException ex) { - Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); - status = Status.ERROR; - } - } - - private enum Status { - PENDING(0), - RUNNING(1), - FINNISHED(2), - ERROR(3); - - Status(int i) { - - } - } -} diff --git a/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java b/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java index a030744..34c3118 100644 --- a/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java +++ b/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java @@ -30,10 +30,10 @@ public class FileChecker implements Observable { private JProgressBar pb; private boolean stop = false; - ArrayList deleted = new ArrayList<>(); - HashMap> changed = new HashMap<>(); + private ArrayList deleted = new ArrayList<>(); + private HashMap> changed = new HashMap<>(); int changedCount = 0; - HashMap> added = new HashMap<>(); + private HashMap> added = new HashMap<>(); int addedCount = 0; long size = 0; @@ -66,7 +66,8 @@ public class FileChecker implements Observable { Mod m = (Mod) abstractMod; for (ModFile mf : m.getFiles()) { - checkFile(mf.getName(), mf); + checkFile(m.getName(), mf); + i++; int finalI = i; SwingUtilities.invokeLater(() -> { @@ -99,16 +100,21 @@ public class FileChecker implements Observable { } private void checkFile(String mod, ModFile mf) { - // TODO: Add mf to Array if Array already exists + ArrayList temp = new ArrayList<>(); + if(!mf.exists()) { - added.put(mod, mf); + if(added.containsKey(mod)) temp =added.get(mod); + temp.add(mf); + added.put(mod, temp); addedCount++; size += mf.getSize(); return; } - if(mf.getLocalSize() != mf.getSize()) { - changed.put(mod, mf); + if(mf.getLocalSize() != mf.getSize() || !mf.getSHA1Sum().equalsIgnoreCase(mf.getLocalGeneratedSHA1Sum())) { + if(changed.containsKey(mod)) temp =changed.get(mod); + temp.add(mf); + changed.put(mod, temp); changedCount++; size += mf.getSize(); return; diff --git a/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java b/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java index 68f09bb..51f7240 100644 --- a/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java +++ b/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java @@ -151,16 +151,18 @@ public class RepositoryManger implements Observable { Iterator keys = content.keys(); while (keys.hasNext()) { String modfile = keys.next(); - long modfilesize = content.getLong(modfile); + JSONObject jo = content.getJSONObject(modfile); + long modfilesize = jo.getLong("size"); + String sha1 = jo.getString("sha1"); - modFiles.add(new ModFile(new File(finalModPath + File.separator + modname + File.separator + modfile), modfile, modfilesize)); + modFiles.add(new ModFile(new File(finalModPath + File.separator + modname + File.separator + modfile), modfile, modname, modfilesize, sha1)); RepositoryManger.MOD_LIST_SIZE++; } MOD_LIST.add(new Mod(modname, modsize, modFiles)); } else { // Single File - MOD_LIST.add(new ModFile(new File(finalModPath + File.separator + modname), modname, modsize)); + MOD_LIST.add(new ModFile(new File(finalModPath + File.separator + modname), modname, modsize, jsonMod.getString("sha1"))); RepositoryManger.MOD_LIST_SIZE++; } diff --git a/src/main/java/de/mc8051/arma3launcher/repo/SyncList.java b/src/main/java/de/mc8051/arma3launcher/repo/SyncList.java new file mode 100644 index 0000000..900cbfb --- /dev/null +++ b/src/main/java/de/mc8051/arma3launcher/repo/SyncList.java @@ -0,0 +1,108 @@ +package de.mc8051.arma3launcher.repo; + +import de.mc8051.arma3launcher.objects.AbstractMod; +import de.mc8051.arma3launcher.objects.Mod; +import de.mc8051.arma3launcher.objects.ModFile; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +/** + * Created by gurkengewuerz.de on 26.03.2020. + */ +public class SyncList extends ArrayList { + + private long size = 0; + private int count = 0; + + private ArrayList deleted = new ArrayList<>(); + + private SyncList(long size, int count) { + this.size = size; + this.count = count; + } + + public SyncList() { + + } + + public void setDeleted(ArrayList deleted) { + this.deleted = deleted; + } + + @Override + public boolean add(AbstractMod abstractMod) { + addedMod(abstractMod); + return super.add(abstractMod); + } + + @Override + public boolean addAll(Collection c) { + for (AbstractMod abstractMod : c) { + addedMod(abstractMod); + } + return super.addAll(c); + } + + private void addedMod(AbstractMod abstractMod) { + if (abstractMod instanceof Mod) { + Mod mod = (Mod) abstractMod; + for (ModFile mf : mod.getFiles()) { + size += mf.getSize(); + count++; + } + } else if (abstractMod instanceof ModFile) { + ModFile mf = (ModFile) abstractMod; + size += mf.getSize(); + count++; + } + } + + @Override + public AbstractMod remove(int index) { + AbstractMod abstractMod = get(index); + + if (abstractMod instanceof Mod) { + Mod mod = (Mod) abstractMod; + for (ModFile mf : mod.getFiles()) { + size -= mf.getSize(); + } + } else if (abstractMod instanceof ModFile) { + ModFile mf = (ModFile) abstractMod; + size -= mf.getSize(); + } + + return super.remove(index); + } + + public void setSize(long size) { + this.size = size; + } + + public void setCount(int count) { + this.count = count; + } + + public SyncList clone() { + SyncList clone = new SyncList(); + clone.addAll(this); + clone.getDeleted().addAll(deleted); + clone.setSize(size); + clone.setDeleted(deleted); + return clone; + } + + public ArrayList getDeleted() { + return deleted; + } + + public long getSize() { + return size; + } + + public int getCount() { + return count; + } +} diff --git a/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java b/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java index 0b058f4..59a09db 100644 --- a/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java +++ b/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java @@ -1,8 +1,304 @@ package de.mc8051.arma3launcher.repo; +import co.bitshfted.xapps.zsync.Zsync; +import co.bitshfted.xapps.zsync.ZsyncException; +import co.bitshfted.xapps.zsync.ZsyncObserver; +import de.mc8051.arma3launcher.ArmA3Launcher; +import de.mc8051.arma3launcher.LauncherGUI; +import de.mc8051.arma3launcher.interfaces.Observable; +import de.mc8051.arma3launcher.interfaces.Observer; +import de.mc8051.arma3launcher.objects.AbstractMod; +import de.mc8051.arma3launcher.objects.ModFile; +import humanize.Humanize; + +import javax.swing.*; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * Created by gurkengewuerz.de on 25.03.2020. */ -public class Syncer { -// FilenameUtils.directoryContains +public class Syncer extends ZsyncObserver implements Observable { + + private List observerList = new ArrayList<>(); + + private boolean stopped = false; + private boolean paused = false; + private boolean running = false; + + private ModFile currentDownload = null; + private SyncList modlist; + + private boolean currentDownload_failed = false; + private boolean controlfile_downloaded = false; + private int failed = 0; + private int success = 0; + + long syncSize; + int syncCount; + + long downloadStarted; + long downloadEnded; + long downloadSize; + long downloadDownloaded; + + private Zsync zsync; + private LauncherGUI gui; + + public Syncer(LauncherGUI gui) { + zsync = new Zsync(); + this.gui = gui; + } + + public void sync(SyncList ml) { + modlist = ml; + + stopped = false; + paused = false; + running = true; + + currentDownload = null; + + failed = 0; + success = 0; + + syncSize = ml.getSize(); + syncCount = ml.getCount(); + SwingUtilities.invokeLater(() -> { + gui.syncDownloadProgress.setMaximum(syncCount); + gui.syncDownloadProgress.setValue(0); + }); + + boolean lastPause = false; + while (running) { + if (stopped) { + running = false; + break; + } + if (modlist.isEmpty()) { + running = false; + break; + } + + if (paused) { + if (!lastPause) { + lastPause = true; + notifyObservers("syncPaused"); + } + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); + } + continue; + } else if (lastPause) { + lastPause = false; + notifyObservers("syncContinue"); + } + + if (currentDownload != null) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); + } + } + + AbstractMod abstractMod = modlist.get(0); + + ModFile mf = null; + if (abstractMod instanceof ModFile) { + mf = (ModFile) abstractMod; + } + + if (mf != null) { + Zsync.Options o = new Zsync.Options(); + o.setOutputFile(Paths.get(mf.getLocaleFile().getAbsolutePath())); + + try { + currentDownload = mf; + currentDownload_failed = false; + controlfile_downloaded = false; + + zsync.zsync(URI.create(mf.getRemoteFile() + ".zsync"), o, this); + } catch (ZsyncException | IllegalArgumentException e) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); + } + } else { + modlist.remove(0); + } + } + + deleteFiles(); + cleanUpEmptyFolders(); + + if (stopped) { + notifyObservers("syncStopped"); + } else { + notifyObservers("syncComplete"); + } + } + + public void finnishCurrent() { + modlist.remove(0); + currentDownload = null; + } + + public void deleteFiles() { + modlist.getDeleted().stream() + .filter((p) -> p.toFile().exists()) + .filter((p) -> p.toFile().canRead()) + .filter((p) -> p.toFile().canWrite()) + .forEach((p) -> p.toFile().delete()); + } + + public void cleanUpEmptyFolders() { + try { + String modPath = ArmA3Launcher.user_config.get("client", "modPath"); + if (modPath == null) modPath = ""; + if (modPath.isEmpty()) return; + Files.find(Paths.get(modPath), + Integer.MAX_VALUE, + (filePath, fileAttr) -> fileAttr.isDirectory()) + .filter((p) -> p.toFile().canRead()) + .filter((p) -> p.toFile().canWrite()) + .filter((p) -> p.toFile().list().length == 0) + .forEach((p) -> p.toFile().delete()); + } catch (IOException e) { + Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); + } + } + + @Override + public void zsyncStarted(URI requestedZsyncUri, Zsync.Options options) { + super.zsyncStarted(requestedZsyncUri, options); + System.out.println("ZSync started " + options.getOutputFile()); + } + + @Override + public void controlFileDownloadingComplete() { + super.controlFileDownloadingComplete(); + System.out.println("controlFileDownloadingComplete"); + controlfile_downloaded = true; + } + + @Override + public void zsyncFailed(Exception exception) { + super.zsyncFailed(exception); + currentDownload_failed = true; + System.out.println("Zsync failed " + exception.getMessage()); + } + + @Override + public void zsyncComplete() { + super.zsyncComplete(); + + downloadEnded = System.nanoTime(); + System.out.println(downloadSize); + System.out.println(downloadEnded - downloadStarted); + System.out.println((downloadSize / (downloadEnded - downloadStarted)) / 1000); + + System.out.println("Zsync complete"); + + if (currentDownload_failed) + failed++; + else success++; + + final long finalSize = syncSize - modlist.getSize(); + int i = success + failed; + int percentage = (int) ((double)i / (double)Long.valueOf(syncCount).intValue() * 100); + + SwingUtilities.invokeLater(() -> { + gui.syncDownloadProgress.setValue(i); + gui.syncDownloadedLabel.setText(Humanize.binaryPrefix(finalSize) + " " + " (" + failed + " failed)"); + gui.syncDownloadProgress.setString(percentage + "%"); + }); + + finnishCurrent(); + } + + @Override + public void controlFileDownloadingStarted(URI uri, long length) { + super.controlFileDownloadingStarted(uri, length); + System.out.println("controlFileDownloadingStarted " + length); + } + + @Override + public void remoteFileDownloadingStarted(URI uri, long length) { + super.remoteFileDownloadingStarted(uri, length); + System.out.println("remoteFileDownloadingStarted " + length); + + SwingUtilities.invokeLater(() -> { + gui.syncFileProgress.setMaximum(Long.valueOf(length).intValue()); + gui.syncFileProgress.setValue(0); + }); + + downloadSize = length; + downloadDownloaded = 0; + downloadStarted = System.nanoTime(); + } + + @Override + public void bytesDownloaded(long bytes) { + super.bytesDownloaded(bytes); +// System.out.println("Downloaded " + bytes); + downloadDownloaded += bytes; + + // TODO: Fix file Download Progress + if (controlfile_downloaded) { + SwingUtilities.invokeLater(() -> { + gui.syncFileProgress.setValue(Long.valueOf(downloadDownloaded).intValue()); + }); + } + } + + + public boolean isStopped() { + return stopped; + } + + public boolean isPaused() { + return paused; + } + + public boolean isRunning() { + return running; + } + + public void stop() { + this.stopped = true; + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public int getCountFailed() { + return failed; + } + + public int getCountSuccess() { + return success; + } + + @Override + public void addObserver(Observer observer) { + observerList.add(observer); + } + + @Override + public void removeObserver(Observer observer) { + observerList.remove(observer); + } + + @Override + public void notifyObservers(String obj) { + for (Observer obs : observerList) obs.update(obj); + } } diff --git a/src/main/resources/lang_de_DE.properties b/src/main/resources/lang_de_DE.properties index 88ef780..3795b5c 100644 --- a/src/main/resources/lang_de_DE.properties +++ b/src/main/resources/lang_de_DE.properties @@ -88,6 +88,12 @@ collapse_all=Alles einklappen same_mod_arma_dir_msg=Das ArmA sowie Mod Verzeichnis dürfen nicht identisch sein. same_mod_arma_dir=Gleiches Verzeichnis check_local_addons=Lokale Dateien überprüfen -changed_files=Veränderte Datien -added_files=Hinzugefügte Datien -deleted_files=Gelöschte Dateien \ No newline at end of file +changed_filesize=Veränderte Größe +added_files=Hinzugefügte Dateien +deleted_files=Gelöschte Dateien +changed_filesize=Veränderte Größe +changed_filesize_tooltip=Dies ist die ungefähre Dateigröße die Maximal runtergeladen wird +resume=Fortsetzen +changed_filesize=Veränderte Dateien +changed_files=Geänderte Dateien +file_count=Dateianzahl \ No newline at end of file diff --git a/src/main/resources/lang_en_US.properties b/src/main/resources/lang_en_US.properties index 52d559a..294762d 100644 --- a/src/main/resources/lang_en_US.properties +++ b/src/main/resources/lang_en_US.properties @@ -85,4 +85,13 @@ steam_not_running=Steam not running. Please start Steam. repository_content=Repository Content collapse_all=Collapse All same_mod_arma_dir_msg=The ArmA and Mod directory must not be identical. -same_mod_arma_dir=Same directory \ No newline at end of file +same_mod_arma_dir=Same directory +resume=Resume +added_files=Added files +deleted_files=Deleted files +update_repository=Update repository +check_local_addons=Check local files +changed_filesize=Changed size +changed_files=Changed files +changed_filesize_tooltip=This is the approximate maximum file size that will be downloaded +file_count=Number of files \ No newline at end of file