parent
89f1a3f4bb
commit
00e90843f4
@ -1,100 +0,0 @@
|
|||||||
@startuml
|
|
||||||
|
|
||||||
title __ENSYNC's Class Diagram__\n
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Execute {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Flow {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Main {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Tools {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncDirectory {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncFile {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncMap {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace ui {
|
|
||||||
class com.olexyn.ensync.ui.Bridge {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace ui {
|
|
||||||
class com.olexyn.ensync.ui.Controller {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace ui {
|
|
||||||
class com.olexyn.ensync.ui.UI {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
com.olexyn.ensync.Flow .up.|> java.lang.Runnable
|
|
||||||
com.olexyn.ensync.Flow o-- com.olexyn.ensync.Tools : tools
|
|
||||||
com.olexyn.ensync.Tools o-- com.olexyn.ensync.Execute : x
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.Tools : tools
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.Execute : x
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.artifacts.SyncMap : syncMap
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.artifacts.SyncDirectory : thisSD
|
|
||||||
com.olexyn.ensync.artifacts.SyncFile -up-|> java.io.File
|
|
||||||
com.olexyn.ensync.artifacts.SyncFile o-- com.olexyn.ensync.artifacts.SyncDirectory : sd
|
|
||||||
com.olexyn.ensync.artifacts.SyncMap o-- com.olexyn.ensync.Tools : tools
|
|
||||||
com.olexyn.ensync.ui.Controller .up.|> javafx.fxml.Initializable
|
|
||||||
com.olexyn.ensync.ui.Controller o-- com.olexyn.ensync.ui.Bridge : bridge
|
|
||||||
com.olexyn.ensync.ui.UI .up.|> java.lang.Runnable
|
|
||||||
com.olexyn.ensync.ui.UI -up-|> javafx.application.Application
|
|
||||||
|
|
||||||
|
|
||||||
right footer
|
|
||||||
|
|
||||||
|
|
||||||
PlantUML diagram generated by SketchIt! (https://bitbucket.org/pmesmeur/sketch.it)
|
|
||||||
For more information about this tool, please contact philippe.mesmeur@gmail.com
|
|
||||||
endfooter
|
|
||||||
|
|
||||||
@enduml
|
|
@ -1,54 +0,0 @@
|
|||||||
package com.olexyn.ensync;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Execute {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param cmd an array representing a shell command
|
|
||||||
* @return <i>TwoBr</i> class, containing two BufferedReaders,
|
|
||||||
* <i>output</i> and <i>error</i>
|
|
||||||
* @see <i>output</i> BufferedReader, corresponds to STDOUT
|
|
||||||
* <i>error</i> BufferedReader, corresponds to STDERR
|
|
||||||
*/
|
|
||||||
public TwoBr execute(String cmd[]) {
|
|
||||||
TwoBr twobr = new TwoBr();
|
|
||||||
try {
|
|
||||||
Process process = Runtime.getRuntime().exec(cmd);
|
|
||||||
process.waitFor();
|
|
||||||
twobr.output = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
twobr.error = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return twobr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TwoBr execute(List<String> cmd) {
|
|
||||||
|
|
||||||
String[] cmdArr = new String[cmd.size()];
|
|
||||||
for (int i = 0; i < cmd.size(); i++) {
|
|
||||||
cmdArr[i] = cmd.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return execute(cmdArr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void executeBatch(List<String[]> batch) {
|
|
||||||
|
|
||||||
for (String[] strings : batch) {
|
|
||||||
execute(strings);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class TwoBr {
|
|
||||||
public BufferedReader output;
|
|
||||||
public BufferedReader error;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
package com.olexyn.ensync;
|
|
||||||
|
|
||||||
import com.olexyn.ensync.artifacts.SyncDirectory;
|
|
||||||
import com.olexyn.ensync.artifacts.SyncMap;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import static com.olexyn.ensync.Main.MAP_OF_SYNCMAPS;
|
|
||||||
|
|
||||||
public class Flow implements Runnable {
|
|
||||||
|
|
||||||
|
|
||||||
Tools tools = new Tools();
|
|
||||||
|
|
||||||
|
|
||||||
private String state;
|
|
||||||
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
|
|
||||||
|
|
||||||
synchronized (MAP_OF_SYNCMAPS) {
|
|
||||||
readOrMakeStateFile();
|
|
||||||
|
|
||||||
for (var syncMapEntry : MAP_OF_SYNCMAPS.entrySet()) {
|
|
||||||
|
|
||||||
|
|
||||||
for (var SDEntry : syncMapEntry.getValue().syncDirectories.entrySet()) {
|
|
||||||
|
|
||||||
SyncDirectory SD = SDEntry.getValue();
|
|
||||||
|
|
||||||
state = "READ";
|
|
||||||
SD.readFreshState();
|
|
||||||
|
|
||||||
SD.listCreated = SD.makeListCreated();
|
|
||||||
SD.listDeleted = SD.makeListDeleted();
|
|
||||||
SD.listModified = SD.makeListModified();
|
|
||||||
|
|
||||||
SD.doCreate();
|
|
||||||
SD.doDelete();
|
|
||||||
SD.doModify();
|
|
||||||
|
|
||||||
SD.writeStateFile(SD.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
long pause = 2000;
|
|
||||||
System.out.println("Pausing... for "+pause+ "ms.");
|
|
||||||
Thread.sleep(pause);
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String getState() {
|
|
||||||
return state == null ? "NONE" : state;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For every single SyncDirectory try to read it's StateFile. <p>
|
|
||||||
* If the StateFile is missing, then create a StateFile.
|
|
||||||
*/
|
|
||||||
private void readOrMakeStateFile() {
|
|
||||||
for (var syncMapEntry : MAP_OF_SYNCMAPS.entrySet()) {
|
|
||||||
SyncMap syncMap = syncMapEntry.getValue();
|
|
||||||
state = syncMap.toString();
|
|
||||||
|
|
||||||
for (var stringSyncDirectoryEntry : syncMap.syncDirectories.entrySet()) {
|
|
||||||
SyncDirectory SD = stringSyncDirectoryEntry.getValue();
|
|
||||||
String path = SD.path;
|
|
||||||
String stateFilePath = tools.stateFilePath(path);
|
|
||||||
|
|
||||||
if (new File(stateFilePath).exists()) {
|
|
||||||
state = "READ-STATE-FILE-" + SD.readStateFile();
|
|
||||||
} else {
|
|
||||||
SD.writeStateFile(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package com.olexyn.ensync;
|
|
||||||
|
|
||||||
import com.olexyn.ensync.artifacts.SyncMap;
|
|
||||||
import com.olexyn.ensync.ui.UI;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
|
|
||||||
public class Main{
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
final public static Thread UI_THREAD = new Thread(new UI(), "ui");
|
|
||||||
|
|
||||||
final public static Thread FLOW_THREAD = new Thread(new Flow(), "flow");
|
|
||||||
|
|
||||||
final public static HashMap<String, SyncMap> MAP_OF_SYNCMAPS = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
UI_THREAD.start();
|
|
||||||
|
|
||||||
FLOW_THREAD.start();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
package com.olexyn.ensync;
|
|
||||||
|
|
||||||
import com.olexyn.ensync.artifacts.SyncFile;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class Tools {
|
|
||||||
|
|
||||||
private final Execute x;
|
|
||||||
|
|
||||||
public Tools() {
|
|
||||||
x = new Execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert BufferedReader to String.
|
|
||||||
*
|
|
||||||
* @param br BufferedReader
|
|
||||||
* @return String
|
|
||||||
*/
|
|
||||||
public String brToString(BufferedReader br) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
Object[] br_array = br.lines().toArray();
|
|
||||||
for (int i = 0; i < br_array.length; i++) {
|
|
||||||
sb.append(br_array[i].toString() + "\n");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert BufferedReader to List of Strings.
|
|
||||||
*
|
|
||||||
* @param br BufferedReader
|
|
||||||
* @return List
|
|
||||||
*/
|
|
||||||
public List<String> brToListString(BufferedReader br) {
|
|
||||||
List<String> list = new ArrayList<>();
|
|
||||||
Object[] br_array = br.lines().toArray();
|
|
||||||
for (int i = 0; i < br_array.length; i++) {
|
|
||||||
list.add(br_array[i].toString());
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<String> fileToLines(File file) {
|
|
||||||
String filePath = file.getPath();
|
|
||||||
List<String> lines = null;
|
|
||||||
try {
|
|
||||||
lines = Files.readAllLines(Paths.get(filePath));
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String fileToString(File file){
|
|
||||||
List<String> lineList = fileToLines(file);
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (String line : lineList){
|
|
||||||
sb.append(line).append("\n");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Map<String, SyncFile> mapMinus(Map<String, SyncFile> fromA, Map<String, SyncFile> substractB) {
|
|
||||||
|
|
||||||
Map<String, SyncFile> difference = new HashMap<>();
|
|
||||||
for (Map.Entry<String, SyncFile> entry : fromA.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
|
|
||||||
if (fromA.containsKey(key) && !substractB.containsKey(key)) {
|
|
||||||
SyncFile file = fromA.get(key);
|
|
||||||
difference.put(key, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return difference;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public StringBuilder stringListToSb(List<String> list) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
for (String line : list) {
|
|
||||||
sb.append(line + "\n");
|
|
||||||
}
|
|
||||||
return sb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write sb to file at path .
|
|
||||||
*
|
|
||||||
* @param path <i>String</i>
|
|
||||||
* @param sb <i>StringBuilder</i>
|
|
||||||
*/
|
|
||||||
public void writeSbToFile(String path, StringBuilder sb) {
|
|
||||||
try {
|
|
||||||
BufferedWriter bw = new BufferedWriter(new FileWriter(new File(path)));
|
|
||||||
bw.write(sb.toString());
|
|
||||||
bw.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write List of String to file at path .
|
|
||||||
*
|
|
||||||
* @param path <i>String</i>
|
|
||||||
* @param list <i>StringBuilder</i>
|
|
||||||
*/
|
|
||||||
public void writeStringListToFile(String path, List<String> list) {
|
|
||||||
File file = new File(path);
|
|
||||||
File parent = new File(file.getParent());
|
|
||||||
if (!parent.exists()) {
|
|
||||||
|
|
||||||
x.execute(new String[]{"mkdir",
|
|
||||||
"-p",
|
|
||||||
parent.getPath()});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
BufferedWriter bw = new BufferedWriter(new FileWriter(new File(path)));
|
|
||||||
StringBuilder sb = stringListToSb(list);
|
|
||||||
bw.write(sb.toString());
|
|
||||||
bw.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String stateFilePath(String path) {
|
|
||||||
return "/tmp/ensync/state" + path.replace("/", "-");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,362 +0,0 @@
|
|||||||
package com.olexyn.ensync.artifacts;
|
|
||||||
|
|
||||||
import com.olexyn.ensync.Execute;
|
|
||||||
import com.olexyn.ensync.Tools;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A SyncDirectory is an occurrence of a particular directory somewhere across the filesystems.
|
|
||||||
*/
|
|
||||||
public class SyncDirectory {
|
|
||||||
|
|
||||||
private String flowState;
|
|
||||||
private SyncDirectory thisSD = this;
|
|
||||||
|
|
||||||
|
|
||||||
private SyncMap syncMap;
|
|
||||||
public String path = null;
|
|
||||||
|
|
||||||
public Map<String, SyncFile> listCreated = new HashMap<>();
|
|
||||||
public Map<String, SyncFile> listDeleted = new HashMap<>();
|
|
||||||
public Map<String, SyncFile> listModified = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
Tools tools = new Tools();
|
|
||||||
Execute x = new Execute();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a SyncDirectory from realPath.
|
|
||||||
*
|
|
||||||
* @see SyncMap
|
|
||||||
*/
|
|
||||||
public SyncDirectory(String path, SyncMap syncMap) {
|
|
||||||
|
|
||||||
this.path = path;
|
|
||||||
this.syncMap = syncMap;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current state by using the `find` command.
|
|
||||||
*/
|
|
||||||
public Map<String, SyncFile> readFreshState() {
|
|
||||||
//NOTE that the SFile().lastModifiedOld is not set here, so it is 0 by default.
|
|
||||||
Map<String, SyncFile> filemap = new HashMap<>();
|
|
||||||
|
|
||||||
Execute.TwoBr find = x.execute(new String[]{"find",
|
|
||||||
path});
|
|
||||||
|
|
||||||
List<String> pathList = tools.brToListString(find.output);
|
|
||||||
|
|
||||||
for (String filePath : pathList) {
|
|
||||||
SyncFile file = new SyncFile(this, filePath);
|
|
||||||
|
|
||||||
filemap.put(filePath, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return filemap;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* READ the contents of StateFile to Map.
|
|
||||||
*/
|
|
||||||
public Map<String, SyncFile> readStateFile() {
|
|
||||||
Map<String, SyncFile> filemap = new HashMap<>();
|
|
||||||
List<String> lines = tools.fileToLines(new File(tools.stateFilePath(path)));
|
|
||||||
|
|
||||||
for (String line : lines) {
|
|
||||||
// this is a predefined format: "modification-time path"
|
|
||||||
String modTimeString = line.split(" ")[0];
|
|
||||||
long modTime = Long.parseLong(modTimeString);
|
|
||||||
|
|
||||||
String sFilePath = line.replace(modTimeString + " ", "");
|
|
||||||
SyncFile sfile = new SyncFile(this, sFilePath);
|
|
||||||
|
|
||||||
sfile.setTimeModifiedFromStateFile(modTime);
|
|
||||||
|
|
||||||
filemap.put(sFilePath, sfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filemap;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare the OLD and NEW pools.
|
|
||||||
* List is cleared and created each time.
|
|
||||||
*/
|
|
||||||
public Map<String, SyncFile> makeListCreated() {
|
|
||||||
|
|
||||||
Map<String, SyncFile> fromA = readFreshState();
|
|
||||||
Map<String, SyncFile> substractB = readStateFile();
|
|
||||||
|
|
||||||
return tools.mapMinus(fromA, substractB);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare the OLD and NEW pools.
|
|
||||||
* List is cleared and created each time.
|
|
||||||
*/
|
|
||||||
public Map<String, SyncFile> makeListDeleted() {
|
|
||||||
|
|
||||||
Map<String, SyncFile> fromA = readStateFile();
|
|
||||||
Map<String, SyncFile> substractB = readFreshState();
|
|
||||||
|
|
||||||
Map<String, SyncFile> listDeleted = tools.mapMinus(fromA, substractB);
|
|
||||||
|
|
||||||
Map<String, SyncFile> swap = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
for (var entry : listDeleted.entrySet()) {
|
|
||||||
|
|
||||||
String key = entry.getKey();
|
|
||||||
String parentKey = entry.getValue().getParent();
|
|
||||||
|
|
||||||
if (listDeleted.containsKey(parentKey) || swap.containsKey(parentKey)) {
|
|
||||||
swap.put(key, listDeleted.get(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tools.mapMinus(listDeleted, swap);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare the OLD and NEW pools.
|
|
||||||
* List is cleared and created each time.
|
|
||||||
*/
|
|
||||||
public Map<String, SyncFile> makeListModified() {
|
|
||||||
|
|
||||||
Map<String, SyncFile> listModified = new HashMap<>();
|
|
||||||
|
|
||||||
Map<String, SyncFile> stateFileMap = readStateFile();
|
|
||||||
|
|
||||||
for (var freshFileEntry : readFreshState().entrySet()) {
|
|
||||||
|
|
||||||
String freshFileKey = freshFileEntry.getKey();
|
|
||||||
SyncFile freshFile = freshFileEntry.getValue();
|
|
||||||
|
|
||||||
if (freshFile.isDirectory()) { continue;} // no need to modify Directories, the Filesystem will do that, if a File changed.
|
|
||||||
|
|
||||||
// If KEY exists in OLD , thus FILE was NOT created.
|
|
||||||
boolean oldFileExists = stateFileMap.containsKey(freshFileKey);
|
|
||||||
boolean fileIsFresher = freshFile.getTimeModified() > freshFile.getTimeModifiedFromStateFile();
|
|
||||||
|
|
||||||
if (oldFileExists && fileIsFresher) {
|
|
||||||
listModified.put(freshFileKey, freshFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return listModified;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* QUERY state of the filesystem at realPath.
|
|
||||||
* WRITE the state of the filesystem to file.
|
|
||||||
*/
|
|
||||||
public void writeStateFile(String path) {
|
|
||||||
List<String> outputList = new ArrayList<>();
|
|
||||||
|
|
||||||
|
|
||||||
Execute.TwoBr find = x.execute(new String[]{"find",
|
|
||||||
path});
|
|
||||||
|
|
||||||
List<String> pathList = tools.brToListString(find.output);
|
|
||||||
|
|
||||||
|
|
||||||
for (String filePath : pathList) {
|
|
||||||
long lastModified = new File(filePath).lastModified();
|
|
||||||
outputList.add("" + lastModified + " " + filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
tools.writeStringListToFile(tools.stateFilePath(path), outputList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class Info {
|
|
||||||
|
|
||||||
private String thisFilePath;
|
|
||||||
private String otherFilePath;
|
|
||||||
private String otherParentPath;
|
|
||||||
private File otherParentFile;
|
|
||||||
|
|
||||||
|
|
||||||
private Info(SyncDirectory thisSD, SyncFile sFile, SyncDirectory otherSD) {
|
|
||||||
// Example:
|
|
||||||
// syncDirectory /foo
|
|
||||||
// otherSyncDirectory /bar
|
|
||||||
// createdFile /foo/hello/created-file.gif
|
|
||||||
// relativePath /hello/created-file.gif
|
|
||||||
String relativePath = sFile.getPath().replace(thisSD.path, "");
|
|
||||||
this.thisFilePath = sFile.getPath();
|
|
||||||
this.otherFilePath = otherSD.path + relativePath;
|
|
||||||
File otherFile = new File(otherFilePath);
|
|
||||||
|
|
||||||
this.otherParentPath = otherFile.getParent();
|
|
||||||
this.otherParentFile = new File(otherParentPath);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void doCreate() {
|
|
||||||
|
|
||||||
for (var entry : listCreated.entrySet()) {
|
|
||||||
SyncFile createdFile = entry.getValue();
|
|
||||||
|
|
||||||
for (var otherEntry : syncMap.syncDirectories.entrySet()) {
|
|
||||||
SyncDirectory otherSD = otherEntry.getValue();
|
|
||||||
|
|
||||||
if (this.equals(otherSD)) { continue; }
|
|
||||||
|
|
||||||
Info info = new Info(this, createdFile, otherSD);
|
|
||||||
|
|
||||||
writeFile(info, this, createdFile, otherSD);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public void doDelete() {
|
|
||||||
|
|
||||||
for (var entry : listDeleted.entrySet()) {
|
|
||||||
SyncFile deletedFile = entry.getValue();
|
|
||||||
|
|
||||||
for (var otherEntry : syncMap.syncDirectories.entrySet()) {
|
|
||||||
SyncDirectory otherSD = otherEntry.getValue();
|
|
||||||
|
|
||||||
if (this.equals(otherSD)) { continue; }
|
|
||||||
|
|
||||||
Info info = new Info(thisSD, deletedFile, otherSD);
|
|
||||||
deleteFile(info, thisSD, deletedFile, otherSD);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteFile(Info info, SyncDirectory thisSD, SyncFile thisFile, SyncDirectory otherSD) {
|
|
||||||
|
|
||||||
SyncFile otherFile = new SyncFile(otherSD, otherSD.path + thisFile.relativePath);
|
|
||||||
|
|
||||||
if (!otherFile.exists()) { return;}
|
|
||||||
|
|
||||||
// if the otherFile was created with ensync it will have the == TimeModified.
|
|
||||||
long thisFileTimeModified = thisFile.getTimeModified();
|
|
||||||
long otherFileTimeModified = otherFile.getTimeModified();
|
|
||||||
|
|
||||||
if (thisFile.getTimeModified() >= otherFile.getTimeModified()) {
|
|
||||||
List<String> cmd = List.of("rm", "-r", info.otherFilePath);
|
|
||||||
x.execute(cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void doModify() {
|
|
||||||
|
|
||||||
for (var entry : listModified.entrySet()) {
|
|
||||||
SyncFile modifiedFile = entry.getValue();
|
|
||||||
|
|
||||||
for (var otherEntry : syncMap.syncDirectories.entrySet()) {
|
|
||||||
SyncDirectory otherSD = otherEntry.getValue();
|
|
||||||
|
|
||||||
if (this.equals(otherSD)) { continue; }
|
|
||||||
|
|
||||||
Info info = new Info(this, modifiedFile, otherSD);
|
|
||||||
|
|
||||||
writeFile(info, this, modifiedFile, otherSD);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void writeFile(Info info, SyncDirectory thisSD, SyncFile thisFile, SyncDirectory otherSD) {
|
|
||||||
|
|
||||||
SyncFile otherFile = new SyncFile(otherSD, otherSD.path + thisFile.relativePath);
|
|
||||||
|
|
||||||
|
|
||||||
if (otherFile.exists() && thisFile.getTimeModified() < otherFile.getTimeModified()) { return;}
|
|
||||||
|
|
||||||
|
|
||||||
if (thisFile.isDirectory() && !otherFile.exists()) {
|
|
||||||
List<String> cmd = List.of("mkdir", "-p", info.otherFilePath);
|
|
||||||
x.execute(cmd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thisFile.isFile()) {
|
|
||||||
|
|
||||||
|
|
||||||
if (!info.otherParentFile.exists()) {
|
|
||||||
makeParentChain(otherFile, thisFile);
|
|
||||||
// List<String> cmd = List.of("mkdir", "-p", info.otherParentPath);
|
|
||||||
//x.execute(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> cmd = List.of("cp", "-p", info.thisFilePath, info.otherFilePath);
|
|
||||||
x.execute(cmd);
|
|
||||||
copyModifDate(thisFile.getParentFile(), otherFile.getParentFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void makeParentChain(File otherFile, File thisFile) {
|
|
||||||
try {
|
|
||||||
File otherParent = new File(otherFile.getParent());
|
|
||||||
File thisParent = new File(thisFile.getParent());
|
|
||||||
|
|
||||||
if (!otherParent.exists()) {
|
|
||||||
makeParentChain(otherParent, thisParent);
|
|
||||||
makeParentChain(otherFile, thisFile);
|
|
||||||
|
|
||||||
} else if (thisFile.isDirectory()) {
|
|
||||||
|
|
||||||
List<String> cmd = List.of("mkdir", otherFile.getPath());
|
|
||||||
x.execute(cmd);
|
|
||||||
|
|
||||||
|
|
||||||
cmd = List.of("stat", "--format", "%y", thisFile.getPath());
|
|
||||||
|
|
||||||
|
|
||||||
String mDate = x.execute(cmd).output.readLine();
|
|
||||||
|
|
||||||
|
|
||||||
cmd = List.of("touch", "-m", "--date=" + mDate, otherFile.getPath());
|
|
||||||
String error = x.execute(cmd).error.readLine();
|
|
||||||
int br = 0;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void copyModifDate(File fromFile, File toFile) {
|
|
||||||
try {
|
|
||||||
List<String> cmd = List.of("stat", "--format", "%y", fromFile.getPath());
|
|
||||||
String mDate = x.execute(cmd).output.readLine();
|
|
||||||
|
|
||||||
cmd = List.of("touch", "-m", "--date=" + mDate, toFile.getPath());
|
|
||||||
x.execute(cmd);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
|||||||
package com.olexyn.ensync.artifacts;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class SyncFile extends File {
|
|
||||||
|
|
||||||
|
|
||||||
// Very IMPORTANT field. Allows to store lastModified as it is stored in the StateFile.
|
|
||||||
public long timeModifiedFromStateFile = 0;
|
|
||||||
|
|
||||||
public String relativePath;
|
|
||||||
private SyncDirectory sd;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public SyncFile(SyncDirectory sd , String pathname) {
|
|
||||||
|
|
||||||
super(pathname);
|
|
||||||
this.sd = sd;
|
|
||||||
relativePath = this.getPath().replace(sd.path, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void setTimeModifiedFromStateFile(long value){
|
|
||||||
timeModifiedFromStateFile = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public long getTimeModifiedFromStateFile(){
|
|
||||||
SyncFile record = sd.readStateFile().get(this.getPath());
|
|
||||||
|
|
||||||
|
|
||||||
return record == null ? 0 : record.timeModifiedFromStateFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If File exists on Disk get the TimeModified from there.
|
|
||||||
* Else try to read it from StateFile.
|
|
||||||
* Else return 0 ( = oldest possible time - a value of 0 can be seen as equal to "never existed").
|
|
||||||
* EXAMPLES:
|
|
||||||
* If a File was deleted, then the time will be taken from statefile.
|
|
||||||
* If a File never existed, it will have time = 0, and thus will always be overwritten.
|
|
||||||
*/
|
|
||||||
public long getTimeModified(){
|
|
||||||
if (this.exists()){
|
|
||||||
return lastModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sd.readStateFile().get(this.getPath())!=null){
|
|
||||||
return getTimeModifiedFromStateFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package com.olexyn.ensync.artifacts;
|
|
||||||
|
|
||||||
|
|
||||||
import com.olexyn.ensync.Tools;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A SyncMap is the set of such SyncDirectories. The purpose of the SyncMap is to help syncronize the SyncDirectories.
|
|
||||||
*/
|
|
||||||
public class SyncMap {
|
|
||||||
|
|
||||||
public String name;
|
|
||||||
public Map<String, SyncDirectory> syncDirectories = new HashMap<>();
|
|
||||||
|
|
||||||
Tools tools = new Tools();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see SyncMap
|
|
||||||
*/
|
|
||||||
public SyncMap(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Syncdirectory. <p>
|
|
||||||
* Adds the created SyncDirectory to this SyncMap.
|
|
||||||
*
|
|
||||||
* @param realPath the path from which the SyncDirectory is created.
|
|
||||||
* @see SyncDirectory
|
|
||||||
*/
|
|
||||||
public void addDirectory(String realPath) {
|
|
||||||
if (new File(realPath).isDirectory()) {
|
|
||||||
syncDirectories.put(realPath, new SyncDirectory(realPath, this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeDirectory(String realPath) {
|
|
||||||
syncDirectories.remove(realPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
@startuml
|
|
||||||
|
|
||||||
title __ARTIFACTS's Class Diagram__\n
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncDirectory {
|
|
||||||
+ listCreated : Map<String, SyncFile>
|
|
||||||
+ listDeleted : Map<String, SyncFile>
|
|
||||||
+ listModified : Map<String, SyncFile>
|
|
||||||
+ path : String
|
|
||||||
- flowState : String
|
|
||||||
+ SyncDirectory()
|
|
||||||
+ doCreate()
|
|
||||||
+ doDelete()
|
|
||||||
+ doModify()
|
|
||||||
+ makeListCreated()
|
|
||||||
+ makeListDeleted()
|
|
||||||
+ makeListModified()
|
|
||||||
+ readFreshState()
|
|
||||||
+ readStateFile()
|
|
||||||
+ writeStateFile()
|
|
||||||
- copyModifDate()
|
|
||||||
- deleteFile()
|
|
||||||
- makeParentChain()
|
|
||||||
- writeFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncDirectory.Info {
|
|
||||||
- otherFilePath : String
|
|
||||||
- otherParentFile : File
|
|
||||||
- otherParentPath : String
|
|
||||||
- thisFilePath : String
|
|
||||||
- Info()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncFile {
|
|
||||||
+ relativePath : String
|
|
||||||
+ timeModifiedFromStateFile : long
|
|
||||||
+ SyncFile()
|
|
||||||
+ getTimeModified()
|
|
||||||
+ getTimeModifiedFromStateFile()
|
|
||||||
+ setTimeModifiedFromStateFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace artifacts {
|
|
||||||
class com.olexyn.ensync.artifacts.SyncMap {
|
|
||||||
+ name : String
|
|
||||||
+ syncDirectories : Map<String, SyncDirectory>
|
|
||||||
+ SyncMap()
|
|
||||||
+ addDirectory()
|
|
||||||
+ removeDirectory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.Tools : tools
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.Execute : x
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.artifacts.SyncMap : syncMap
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory o-- com.olexyn.ensync.artifacts.SyncDirectory : thisSD
|
|
||||||
com.olexyn.ensync.artifacts.SyncDirectory +-down- com.olexyn.ensync.artifacts.SyncDirectory.Info
|
|
||||||
com.olexyn.ensync.artifacts.SyncFile -up-|> java.io.File
|
|
||||||
com.olexyn.ensync.artifacts.SyncFile o-- com.olexyn.ensync.artifacts.SyncDirectory : sd
|
|
||||||
com.olexyn.ensync.artifacts.SyncMap o-- com.olexyn.ensync.Tools : tools
|
|
||||||
|
|
||||||
|
|
||||||
right footer
|
|
||||||
|
|
||||||
|
|
||||||
PlantUML diagram generated by SketchIt! (https://bitbucket.org/pmesmeur/sketch.it)
|
|
||||||
For more information about this tool, please contact philippe.mesmeur@gmail.com
|
|
||||||
endfooter
|
|
||||||
|
|
||||||
@enduml
|
|
@ -1,70 +0,0 @@
|
|||||||
@startuml
|
|
||||||
|
|
||||||
title __ENSYNC's Class Diagram__\n
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Execute {
|
|
||||||
+ execute()
|
|
||||||
+ execute()
|
|
||||||
+ executeBatch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Execute.TwoBr {
|
|
||||||
+ error : BufferedReader
|
|
||||||
+ output : BufferedReader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Flow {
|
|
||||||
- state : String
|
|
||||||
+ getState()
|
|
||||||
+ run()
|
|
||||||
- readOrMakeStateFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Main {
|
|
||||||
{static} + FLOW_THREAD : Thread
|
|
||||||
{static} + MAP_OF_SYNCMAPS : HashMap<String, SyncMap>
|
|
||||||
{static} + UI_THREAD : Thread
|
|
||||||
{static} + main()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
class com.olexyn.ensync.Tools {
|
|
||||||
+ Tools()
|
|
||||||
+ brToListString()
|
|
||||||
+ brToString()
|
|
||||||
+ fileToLines()
|
|
||||||
+ mapMinus()
|
|
||||||
+ stateFilePath()
|
|
||||||
+ stringListToSb()
|
|
||||||
+ writeSbToFile()
|
|
||||||
+ writeStringListToFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
com.olexyn.ensync.Execute +-down- com.olexyn.ensync.Execute.TwoBr
|
|
||||||
com.olexyn.ensync.Flow .up.|> java.lang.Runnable
|
|
||||||
com.olexyn.ensync.Flow o-- com.olexyn.ensync.Tools : tools
|
|
||||||
com.olexyn.ensync.Tools o-- com.olexyn.ensync.Execute : x
|
|
||||||
|
|
||||||
|
|
||||||
right footer
|
|
||||||
|
|
||||||
|
|
||||||
PlantUML diagram generated by SketchIt! (https://bitbucket.org/pmesmeur/sketch.it)
|
|
||||||
For more information about this tool, please contact philippe.mesmeur@gmail.com
|
|
||||||
endfooter
|
|
||||||
|
|
||||||
@enduml
|
|
@ -1,11 +0,0 @@
|
|||||||
<syncmap>
|
|
||||||
<name>SyncMap1</name>
|
|
||||||
<syncdirectory>
|
|
||||||
<name>SyncDirectory1</name>
|
|
||||||
<path>/foo/dir</path>
|
|
||||||
</syncdirectory>
|
|
||||||
<syncdirectory>
|
|
||||||
<name>SyncDirectory2</name>
|
|
||||||
<path>/bar/dir</path>
|
|
||||||
</syncdirectory>
|
|
||||||
</syncmap>
|
|
@ -1,6 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
a=$1
|
|
||||||
b=$2
|
|
||||||
$a | $b
|
|
||||||
|
|
||||||
# this is a pipe
|
|
@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
a=$1
|
|
||||||
b=$2
|
|
||||||
c=$3
|
|
||||||
$a | $b | $c
|
|
||||||
|
|
||||||
# this is a double pipe
|
|
@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
a=$1
|
|
||||||
b=$2
|
|
||||||
c=$3
|
|
||||||
|
|
||||||
$a $b > $c
|
|
@ -1,54 +0,0 @@
|
|||||||
package com.olexyn.ensync.ui;
|
|
||||||
|
|
||||||
|
|
||||||
import com.olexyn.ensync.artifacts.SyncMap;
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import static com.olexyn.ensync.Main.MAP_OF_SYNCMAPS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect the Controller and the Flow
|
|
||||||
*/
|
|
||||||
public class Bridge {
|
|
||||||
|
|
||||||
|
|
||||||
void newCollection(String collectionName) {
|
|
||||||
|
|
||||||
synchronized (MAP_OF_SYNCMAPS) {
|
|
||||||
MAP_OF_SYNCMAPS.put(collectionName, new SyncMap(collectionName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void removeCollection(String collectionName) {
|
|
||||||
synchronized (MAP_OF_SYNCMAPS) {
|
|
||||||
MAP_OF_SYNCMAPS.remove(collectionName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void addDirectory(String collectionName, File diretory) {
|
|
||||||
synchronized (MAP_OF_SYNCMAPS) {
|
|
||||||
MAP_OF_SYNCMAPS.get(collectionName).addDirectory(diretory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
//TODO pause syning when adding
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This works, because a directory, which here is an unique ablsolute path,
|
|
||||||
* is only supposed to present once across entire SYNC.
|
|
||||||
*/
|
|
||||||
void removeDirectory(String directoryAbsolutePath) {
|
|
||||||
//TODO fix ConcurrentModificationException. This will possibly resolve further errors.
|
|
||||||
synchronized (MAP_OF_SYNCMAPS) {
|
|
||||||
for (var syncMap : MAP_OF_SYNCMAPS.entrySet()) {
|
|
||||||
syncMap.getValue().removeDirectory(directoryAbsolutePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,237 +0,0 @@
|
|||||||
package com.olexyn.ensync.ui;
|
|
||||||
|
|
||||||
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.fxml.Initializable;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.stage.DirectoryChooser;
|
|
||||||
import javafx.stage.Window;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Controller class for JavaFX. Contains the application logic.
|
|
||||||
*/
|
|
||||||
public class Controller implements Initializable {
|
|
||||||
|
|
||||||
|
|
||||||
final static int COLUMN_COUNT = 5; // How many columns should the GridPane have? Adjust if necessary.
|
|
||||||
final static Text SPACE = new Text(""); // Placeholder
|
|
||||||
int collection_count = 0;
|
|
||||||
Bridge bridge = new Bridge();
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
protected GridPane gridPane;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
|
||||||
|
|
||||||
Text end = new Text("");
|
|
||||||
end.setId("end");
|
|
||||||
|
|
||||||
Button newCollectionButton = new Button("New Collection");
|
|
||||||
newCollectionButton.setId("newCollectionButton");
|
|
||||||
newCollectionButton.setOnAction(event -> { this.newCollection();});
|
|
||||||
|
|
||||||
gridPane.add(end, 0, 0);
|
|
||||||
|
|
||||||
List<Node> nodeList = new ArrayList<>(gridPane.getChildren());
|
|
||||||
|
|
||||||
List<Node> payload = Arrays.asList(new Text(""), new Text(""), new Text(""), new Text(""), newCollectionButton);
|
|
||||||
|
|
||||||
|
|
||||||
insertPayload(nodeList, payload, "end", 0);
|
|
||||||
redraw(gridPane, nodeList);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void newCollection() {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
String collectionName = "name" + collection_count++;
|
|
||||||
|
|
||||||
bridge.newCollection(collectionName);
|
|
||||||
|
|
||||||
|
|
||||||
TextField collectionStateTextField = new TextField();
|
|
||||||
collectionStateTextField.setText("STATE");
|
|
||||||
collectionStateTextField.setStyle("-fx-text-fill: green");
|
|
||||||
collectionStateTextField.setDisable(true);
|
|
||||||
collectionStateTextField.setId("collectionStateTextField-" + collectionName);
|
|
||||||
|
|
||||||
Button removeCollectionButton = new Button("Remove Collection");
|
|
||||||
removeCollectionButton.setId("removeCollectionButton-" + collectionName);
|
|
||||||
removeCollectionButton.setOnAction(event -> { this.removeCollection(collectionName);});
|
|
||||||
|
|
||||||
TextField addDirectoryTextField = new TextField();
|
|
||||||
addDirectoryTextField.setId("addDirectoryTextField-" + collectionName);
|
|
||||||
|
|
||||||
Button addDirectoryButton = new Button("Add Directory");
|
|
||||||
addDirectoryButton.setId("addDirectoryButton-" + collectionName);
|
|
||||||
addDirectoryButton.setOnAction(event -> { this.addDirectory(collectionName);});
|
|
||||||
|
|
||||||
|
|
||||||
List<Node> nodeList = new ArrayList<>(gridPane.getChildren());
|
|
||||||
|
|
||||||
List<Node> payload = new ArrayList<>();
|
|
||||||
payload.addAll(Arrays.asList(new Text(""), new Text(""), collectionStateTextField, new Text(""), removeCollectionButton));
|
|
||||||
payload.addAll(Arrays.asList(addDirectoryTextField, new Text(""), new Text(""), new Text(""), addDirectoryButton));
|
|
||||||
|
|
||||||
insertPayload(nodeList, payload, "newCollectionButton", -4);
|
|
||||||
redraw(gridPane, nodeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For the order & number of Nodes see ui-design.png .
|
|
||||||
* Remove the "Remove-Collection-Line" and the consecutive lines until and including the "Add-Directory-Line".
|
|
||||||
* The !=null expression protects from Text("") placeholders, who have id==null.
|
|
||||||
*/
|
|
||||||
private void removeCollection(String collectionName) {
|
|
||||||
|
|
||||||
bridge.removeCollection(collectionName);
|
|
||||||
|
|
||||||
List<Node> nodeList = new ArrayList<>(gridPane.getChildren());
|
|
||||||
|
|
||||||
here:
|
|
||||||
for (Node node : nodeList) {
|
|
||||||
|
|
||||||
if (node.getId() != null && node.getId().equals("removeCollectionButton-" + collectionName)) {
|
|
||||||
int i = nodeList.indexOf(node) - 4;
|
|
||||||
while (i < nodeList.size()) {
|
|
||||||
nodeList.remove(i);
|
|
||||||
|
|
||||||
if (nodeList.get(i).getId() != null && nodeList.get(i).getId().equals("addDirectoryButton-" + collectionName)) {
|
|
||||||
nodeList.remove(i);
|
|
||||||
break here;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw(gridPane, nodeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private void addDirectory(String collectionName) {
|
|
||||||
// TODO throw error if other collection already contains absollutepath
|
|
||||||
Window stage = gridPane.getScene().getWindow();
|
|
||||||
|
|
||||||
final DirectoryChooser directoryChooser = new DirectoryChooser();
|
|
||||||
directoryChooser.setTitle("Select Directory.");
|
|
||||||
directoryChooser.setInitialDirectory(new File(System.getProperty("user.home")));
|
|
||||||
|
|
||||||
File directory = directoryChooser.showDialog(stage);
|
|
||||||
|
|
||||||
if (directory != null) {
|
|
||||||
|
|
||||||
bridge.addDirectory(collectionName, directory);
|
|
||||||
|
|
||||||
TextField directoryPathTextField = new TextField();
|
|
||||||
directoryPathTextField.setText(directory.getAbsolutePath());
|
|
||||||
directoryPathTextField.setDisable(true);
|
|
||||||
directoryPathTextField.setId("directoryPathTextField-" + directory.getAbsolutePath());
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TextField directoryStateTextField = new TextField();
|
|
||||||
directoryStateTextField.setText("STATE");
|
|
||||||
directoryStateTextField.setStyle("-fx-text-fill: green");
|
|
||||||
directoryStateTextField.setDisable(true);
|
|
||||||
directoryStateTextField.setId("directoryStateTextField-" + directory.getAbsolutePath());
|
|
||||||
|
|
||||||
Button removeDirectoryButton = new Button("Remove");
|
|
||||||
removeDirectoryButton.setId("removeDirectoryButton-" + directory.getAbsolutePath());
|
|
||||||
removeDirectoryButton.setOnAction(event -> { this.removeDirectory(directory.getAbsolutePath());});
|
|
||||||
|
|
||||||
|
|
||||||
List<Node> nodeList = new ArrayList<>(gridPane.getChildren());
|
|
||||||
List<Node> payload = Arrays.asList(directoryPathTextField, new Text(""), directoryStateTextField, new Text(""), removeDirectoryButton);
|
|
||||||
insertPayload(nodeList, payload, "addDirectoryTextField-" + collectionName, 0);
|
|
||||||
redraw(gridPane, nodeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the Node with @param id.
|
|
||||||
* Insert the contents of the @param payload starting from the last.
|
|
||||||
* This pushes the Node with @param id forward.
|
|
||||||
*/
|
|
||||||
private void insertPayload(List<Node> nodeList, List<Node> payload, String id, int offset) {
|
|
||||||
for (Node node : nodeList) {
|
|
||||||
|
|
||||||
if (node.getId() != null && node.getId().equals(id)) {
|
|
||||||
int i = nodeList.indexOf(node) + offset;
|
|
||||||
|
|
||||||
for (int j = payload.size() - 1; j >= 0; j--) {
|
|
||||||
nodeList.add(i, payload.get(j));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the gridPane and redraw it with contents of nodeList.
|
|
||||||
*/
|
|
||||||
private void redraw(GridPane gridPane, List<Node> nodeList) {
|
|
||||||
gridPane.getChildren().clear();
|
|
||||||
|
|
||||||
int col = 0, row = 0;
|
|
||||||
|
|
||||||
for (Node node : nodeList) {
|
|
||||||
if (nodeList.indexOf(node) % COLUMN_COUNT == 0) {
|
|
||||||
row++;
|
|
||||||
col = 0;
|
|
||||||
}
|
|
||||||
gridPane.add(node, col, row);
|
|
||||||
col++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void removeDirectory(String directoryAbsolutePath) {
|
|
||||||
|
|
||||||
bridge.removeDirectory(directoryAbsolutePath);
|
|
||||||
|
|
||||||
List<Node> nodeList = new ArrayList<>(gridPane.getChildren());
|
|
||||||
|
|
||||||
for (Node node : nodeList) {
|
|
||||||
|
|
||||||
if (node.getId() != null && node.getId().equals("removeDirectoryButton-" +directoryAbsolutePath)) {
|
|
||||||
int i = nodeList.indexOf(node) - 4;
|
|
||||||
for (int j = 0; j < 5; j++) {
|
|
||||||
nodeList.remove(i);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
redraw(gridPane, nodeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
|||||||
package com.olexyn.ensync.ui;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.fxml.FXMLLoader;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
|
|
||||||
|
|
||||||
public class UI extends Application implements Runnable {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Stage primaryStage) throws Exception {
|
|
||||||
Parent root = FXMLLoader.load(getClass().getResource("layout.fxml"));
|
|
||||||
Scene scene = new Scene(root, 500, 500);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
primaryStage.setTitle("EnSync");
|
|
||||||
primaryStage.setScene(scene);
|
|
||||||
primaryStage.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
UI.launch();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<?import javafx.geometry.*?>
|
|
||||||
<?import java.lang.*?>
|
|
||||||
<?import javafx.scene.control.*?>
|
|
||||||
<?import javafx.scene.control.ScrollPane?>
|
|
||||||
<?import javafx.scene.layout.*?>
|
|
||||||
|
|
||||||
<ScrollPane fitToHeight="true" fitToWidth="true" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="394.0" prefWidth="672.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.olexyn.ensync.ui.Controller">
|
|
||||||
<content>
|
|
||||||
<GridPane fx:id="gridPane">
|
|
||||||
<columnConstraints>
|
|
||||||
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" maxWidth="1.7976931348623157E308" minWidth="200.0" prefWidth="200.0" />
|
|
||||||
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER" maxWidth="20.0" minWidth="20.0" prefWidth="20.0" />
|
|
||||||
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER" maxWidth="100.0" minWidth="100.0" prefWidth="100.0" />
|
|
||||||
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER" maxWidth="20.0" minWidth="20.0" prefWidth="20.0" />
|
|
||||||
<ColumnConstraints fillWidth="false" halignment="LEFT" hgrow="NEVER" maxWidth="200.0" minWidth="200.0" prefWidth="200.0" />
|
|
||||||
</columnConstraints>
|
|
||||||
<padding>
|
|
||||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
|
||||||
</padding>
|
|
||||||
<rowConstraints>
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
|
|
||||||
</rowConstraints>
|
|
||||||
</GridPane>
|
|
||||||
</content>
|
|
||||||
</ScrollPane>
|
|
@ -1,59 +0,0 @@
|
|||||||
@startuml
|
|
||||||
|
|
||||||
title __UI's Class Diagram__\n
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace ui {
|
|
||||||
class com.olexyn.ensync.ui.Bridge {
|
|
||||||
~ addDirectory()
|
|
||||||
~ newCollection()
|
|
||||||
~ removeCollection()
|
|
||||||
~ removeDirectory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace ui {
|
|
||||||
class com.olexyn.ensync.ui.Controller {
|
|
||||||
# gridPane : GridPane
|
|
||||||
{static} ~ COLUMN_COUNT : int
|
|
||||||
{static} ~ SPACE : Text
|
|
||||||
~ collection_count : int
|
|
||||||
+ initialize()
|
|
||||||
- addDirectory()
|
|
||||||
- insertPayload()
|
|
||||||
- newCollection()
|
|
||||||
- redraw()
|
|
||||||
- removeCollection()
|
|
||||||
- removeDirectory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace com.olexyn.ensync {
|
|
||||||
namespace ui {
|
|
||||||
class com.olexyn.ensync.ui.UI {
|
|
||||||
+ run()
|
|
||||||
+ start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
com.olexyn.ensync.ui.Controller .up.|> javafx.fxml.Initializable
|
|
||||||
com.olexyn.ensync.ui.Controller o-- com.olexyn.ensync.ui.Bridge : bridge
|
|
||||||
com.olexyn.ensync.ui.UI .up.|> java.lang.Runnable
|
|
||||||
com.olexyn.ensync.ui.UI -up-|> javafx.application.Application
|
|
||||||
|
|
||||||
|
|
||||||
right footer
|
|
||||||
|
|
||||||
|
|
||||||
PlantUML diagram generated by SketchIt! (https://bitbucket.org/pmesmeur/sketch.it)
|
|
||||||
For more information about this tool, please contact philippe.mesmeur@gmail.com
|
|
||||||
endfooter
|
|
||||||
|
|
||||||
@enduml
|
|
@ -1,119 +0,0 @@
|
|||||||
package files;
|
|
||||||
|
|
||||||
import com.olexyn.ensync.Execute;
|
|
||||||
import com.olexyn.ensync.Tools;
|
|
||||||
|
|
||||||
public class FileTest {
|
|
||||||
|
|
||||||
Execute x = new Execute();
|
|
||||||
Tools tools = new Tools();
|
|
||||||
|
|
||||||
private static final String PATH = System.getProperty("user.dir");;
|
|
||||||
|
|
||||||
public static void main(String... args){
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates user activity on disk.
|
|
||||||
*/
|
|
||||||
void createFiles() throws InterruptedException {
|
|
||||||
StringBuilder sbA = new StringBuilder("a");
|
|
||||||
StringBuilder sbB = new StringBuilder("b");
|
|
||||||
|
|
||||||
// dv (deleted-void)
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// dd
|
|
||||||
tools.writeSbToFile(PATH+"/a/dd", sbA);
|
|
||||||
Thread.sleep(10);
|
|
||||||
tools.writeSbToFile(PATH+"/b/dd", sbB);
|
|
||||||
Thread.sleep(10);Thread.sleep(10);
|
|
||||||
x.execute(new String[]{"rm", PATH+"/a/dd"});
|
|
||||||
Thread.sleep(10);
|
|
||||||
x.execute(new String[]{"rm", PATH+"/b/dd"});
|
|
||||||
Thread.sleep(10);
|
|
||||||
|
|
||||||
// dc
|
|
||||||
tools.writeSbToFile(PATH+"/a/dc", sbA);
|
|
||||||
Thread.sleep(10);
|
|
||||||
x.execute(new String[]{"rm", PATH+"/a/dc"});
|
|
||||||
Thread.sleep(10);
|
|
||||||
tools.writeSbToFile(PATH+"/b/dc", sbB);
|
|
||||||
Thread.sleep(10);
|
|
||||||
|
|
||||||
// dm
|
|
||||||
tools.writeSbToFile(PATH+"/a/dm", sbA);
|
|
||||||
Thread.sleep(10);
|
|
||||||
x.execute(new String[]{"rm", PATH+"/a/dm"});
|
|
||||||
Thread.sleep(10);
|
|
||||||
tools.writeSbToFile(PATH+"/b/dm", sbB);
|
|
||||||
Thread.sleep(10);
|
|
||||||
|
|
||||||
// dv (deleted-void)
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// cd
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// cc
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// cm
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// cv (created-void)
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// md
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// mc
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// mm
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if end-state is as desired.
|
|
||||||
* @throws Exception otherwise.
|
|
||||||
*/
|
|
||||||
void fileTest() throws Exception {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Files where the second (= the newer) file was deleted. Thus both files should not exist in the end-state.
|
|
||||||
String[] arrayToDelete = {"/a/dd", "/b/dd" , "/a/cd", "/b/cd", "/a/md", "/b/md"};
|
|
||||||
for (String path : arrayToDelete){
|
|
||||||
if (new TestableFile(path).exists()) throw new Exception();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files where the second (= the newer) file was created or modified. Thus both files should contain "b" in the end-state.
|
|
||||||
String[] arrayToB = {"/a/dc", "/b/dc" , "/a/dm", "/b/dm", "/a/cc", "/b/cc"};
|
|
||||||
for (String path : arrayToB){
|
|
||||||
if (!new TestableFile(path).hasContent("b")) throw new Exception();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Assertion Exception
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package files;
|
|
||||||
|
|
||||||
import com.olexyn.ensync.Tools;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public class TestableFile extends File {
|
|
||||||
|
|
||||||
Tools tools = new Tools();
|
|
||||||
|
|
||||||
|
|
||||||
public TestableFile(String pathname) {
|
|
||||||
super(pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasContent(String s){
|
|
||||||
|
|
||||||
String line = tools.fileToLines(this).get(0);
|
|
||||||
return line.equals(s);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
@startuml
|
|
||||||
|
|
||||||
title __FILES's Class Diagram__\n
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
right footer
|
|
||||||
|
|
||||||
|
|
||||||
PlantUML diagram generated by SketchIt! (https://bitbucket.org/pmesmeur/sketch.it)
|
|
||||||
For more information about this tool, please contact philippe.mesmeur@gmail.com
|
|
||||||
endfooter
|
|
||||||
|
|
||||||
@enduml
|
|
@ -1,26 +0,0 @@
|
|||||||
### Testing Scenario
|
|
||||||
Test two configs:
|
|
||||||
1. FileOps happen while System is down.
|
|
||||||
1. FileOps happen while System is running.
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
|
|
||||||
| Symbol | Description|
|
|
||||||
---|---
|
|
||||||
`a` | File `a` in directory `A`
|
|
||||||
`b` | File `b` in directory `B`
|
|
||||||
`d(x)` | File `x` is deleted.
|
|
||||||
`c(x)` | File `x` is created.
|
|
||||||
`m(x)` | File `x` is modified.
|
|
||||||
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| `Given` | | `When` | | `Then` | |
|
|
||||||
---|---|---|---|---|---
|
|
||||||
| `A` | `B`| `A` | `B`|`A` | `B`|
|
|
||||||
| `a` | | `d(a)` | | | |
|
|
||||||
| `a` | `b` | `d(a)` | `d(b)` | | |
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue