From 5cb6d780538e4f4ba6794352b829b69ec85ef770 Mon Sep 17 00:00:00 2001 From: Lenard Spiecker <17047770+l-spiecker@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:45:42 +0100 Subject: [PATCH 1/2] feat: filesync v1 --- .../sdb2/FileAndDirectoryLocations.java | 10 + .../java/org/zephyrsoft/sdb2/Options.java | 13 ++ .../org/zephyrsoft/sdb2/gui/SongCell.java | 9 +- .../zephyrsoft/sdb2/model/XMLConverter.java | 3 +- .../sdb2/presenter/PresenterWindow.java | 20 +- .../sdb2/remote/FileController.java | 124 ++++++++++++ .../zephyrsoft/sdb2/remote/FileRequest.java | 49 +++++ .../java/org/zephyrsoft/sdb2/remote/MQTT.java | 3 +- .../sdb2/remote/RemoteController.java | 187 ++++++++++-------- .../zephyrsoft/sdb2/remote/RemoteTopic.java | 17 ++ 10 files changed, 350 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/zephyrsoft/sdb2/remote/FileController.java create mode 100644 src/main/java/org/zephyrsoft/sdb2/remote/FileRequest.java diff --git a/src/main/java/org/zephyrsoft/sdb2/FileAndDirectoryLocations.java b/src/main/java/org/zephyrsoft/sdb2/FileAndDirectoryLocations.java index b9cd662f..73bf655e 100644 --- a/src/main/java/org/zephyrsoft/sdb2/FileAndDirectoryLocations.java +++ b/src/main/java/org/zephyrsoft/sdb2/FileAndDirectoryLocations.java @@ -44,6 +44,7 @@ public class FileAndDirectoryLocations { private static final String STATISTICS_FILE_STRING = "statistics.xml"; private static final String DB_FILE_STRING = "db.xml"; private static final String DB_PROPERTIES_FILE_STRING = "db.properties.xml"; + private static final String DB_BLOB_SUBDIR_STRING = "blobs"; private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyy-MM"); @@ -90,6 +91,15 @@ public static String getStatisticsMonthlyExportFileName(LocalDate date) { return getStatisticsDir() + File.separator + YEAR_MONTH.format(date) + ".xls"; } + /** this is used when remote control (via MQTT) is active */ + public static String getDBBlobDir() { + if (Options.getInstance().getBlobDatabaseDir() == null) { + return getDir(DB_BLOB_SUBDIR_STRING, true); + } else { + return getDir(Options.getInstance().getBlobDatabaseDir(), false); + } + } + /** this is used when remote control (via MQTT) is active */ private static String getDBDir() { if (Options.getInstance().getDatabaseDir() == null) { diff --git a/src/main/java/org/zephyrsoft/sdb2/Options.java b/src/main/java/org/zephyrsoft/sdb2/Options.java index 8233edba..12b23769 100644 --- a/src/main/java/org/zephyrsoft/sdb2/Options.java +++ b/src/main/java/org/zephyrsoft/sdb2/Options.java @@ -77,6 +77,11 @@ public static Options getInstance() { @Option(name = "--database", aliases = "-db", metaVar = "", usage = "use this directory as database storage (optional, the default is ~/.songdatabase/db/)") private String databaseDir = null; + private static final String PROP_BLOB_DATABASE_DIR = "blob_database"; + /** this is used when remote control (via MQTT) is active */ + @Option(name = "--blob-database", aliases = "-blob-db", metaVar = "", usage = "use this directory as blob database storage (optional, the default is ~/.songdatabase/blobs/)") + private String blobDatabaseDir = null; + private static final String PROP_SONGS_FILE = "songs-file"; @Argument(metaVar = "", usage = "use this file to load from and save to (optional, the default is ~/.songdatabase/songs/songs.xml)") private String songsFile = null; @@ -177,6 +182,14 @@ private void setDatabaseDir(String databaseDir) { this.databaseDir = databaseDir; } + public String getBlobDatabaseDir() { + return blobDatabaseDir; + } + + private void setBlobDatabaseDir(String blobDatabaseDir) { + this.blobDatabaseDir = blobDatabaseDir; + } + public String getSongsFile() { return songsFile; } diff --git a/src/main/java/org/zephyrsoft/sdb2/gui/SongCell.java b/src/main/java/org/zephyrsoft/sdb2/gui/SongCell.java index ddd5cc5e..661b2967 100644 --- a/src/main/java/org/zephyrsoft/sdb2/gui/SongCell.java +++ b/src/main/java/org/zephyrsoft/sdb2/gui/SongCell.java @@ -23,6 +23,8 @@ import java.awt.Insets; import java.net.MalformedURLException; import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; import javax.swing.ImageIcon; import javax.swing.JLabel; @@ -31,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.zephyrsoft.sdb2.FileAndDirectoryLocations; import org.zephyrsoft.sdb2.model.Song; import org.zephyrsoft.sdb2.util.StringTools; import org.zephyrsoft.sdb2.util.gui.ImageTools; @@ -128,7 +131,11 @@ public void setImage(final String imageUrl, int degreesToRotateRight) { this.image.setVisible(false); } else { try { - ImageIcon imageIcon = new ImageIcon(URI.create(imageUrl).toURL()); + URI imageUri = URI.create(imageUrl); + if (imageUri.getScheme().equals("sdb")){ + imageUri = Paths.get(FileAndDirectoryLocations.getDBBlobDir(), imageUrl.replace("sdb://", "")).toUri(); + } + ImageIcon imageIcon = new ImageIcon(imageUri.toURL()); Image image = imageIcon.getImage(); image = ImageTools.rotate(image, degreesToRotateRight); double factor = (songTitle.getPreferredSize().getHeight() + firstLine.getPreferredSize().getHeight() diff --git a/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java b/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java index ab71fc7a..3b92ad7c 100644 --- a/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java +++ b/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java @@ -21,6 +21,7 @@ import org.zephyrsoft.sdb2.model.settings.SettingsModel; import org.zephyrsoft.sdb2.model.statistics.StatisticsModel; import org.zephyrsoft.sdb2.remote.ChangeReject; +import org.zephyrsoft.sdb2.remote.FileRequest; import org.zephyrsoft.sdb2.remote.PatchRequest; import org.zephyrsoft.sdb2.remote.Patches; import org.zephyrsoft.sdb2.remote.Position; @@ -65,7 +66,7 @@ public static T fromXMLToPersistable(InputStream xmlInpu private static JAXBContext createContext() throws JAXBException { JAXBContext context = JAXBContext.newInstance(SongsModel.class, SettingsModel.class, StatisticsModel.class, Version.class, - PatchRequest.class, Song.class, Patches.class, ChangeReject.class, Position.class); + PatchRequest.class, Song.class, Patches.class, ChangeReject.class, Position.class, FileRequest.class); return context; } diff --git a/src/main/java/org/zephyrsoft/sdb2/presenter/PresenterWindow.java b/src/main/java/org/zephyrsoft/sdb2/presenter/PresenterWindow.java index 530e5a3b..241620d9 100644 --- a/src/main/java/org/zephyrsoft/sdb2/presenter/PresenterWindow.java +++ b/src/main/java/org/zephyrsoft/sdb2/presenter/PresenterWindow.java @@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URI; +import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -34,6 +35,7 @@ import org.jdesktop.core.animation.timing.interpolators.LinearInterpolator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.zephyrsoft.sdb2.FileAndDirectoryLocations; import org.zephyrsoft.sdb2.MainController; import org.zephyrsoft.sdb2.model.AddressablePart; import org.zephyrsoft.sdb2.model.SelectableDisplay; @@ -159,7 +161,9 @@ && noSettingsWereChanged()) { toFront(); return; } else if (presentationPosition == null) { - songView.moveToLine(null, null, true); + if (songView != null ) { + songView.moveToLine(null, null, true); + } this.presentationPosition = null; toFront(); return; @@ -229,8 +233,16 @@ && noSettingsWereChanged()) { } else if (presentable.getImage() != null || ( presentable.getSong() != null && !StringTools.isEmpty(presentable.getSong().getImage()))) { // display the image (fullscreen, but with margin) try { - ImageIcon imageIcon = presentable.getImage() == null ? new ImageIcon(URI.create(presentable.getSong().getImage()).toURL()) - : new ImageIcon(presentable.getImage()); + ImageIcon imageIcon; + if (presentable.getImage() == null) { + URI imageUri = URI.create(presentable.getSong().getImage()); + if (imageUri.getScheme().equals("sdb")){ + imageUri = Paths.get(FileAndDirectoryLocations.getDBBlobDir(), imageUri.toString().replace("sdb://", "")).toUri(); + } + imageIcon = new ImageIcon(imageUri.toURL()); + }else { + imageIcon = new ImageIcon(presentable.getImage()); + } Image image = imageIcon.getImage(); if (presentable.getImage() == null) { image = ImageTools.rotate(image, presentable.getSong().getImageRotationAsInt()); @@ -257,7 +269,7 @@ && noSettingsWereChanged()) { revalidate(); repaint(); - if (presentable.getSong() != null) { + if (presentable.getSong() != null && StringTools.isEmpty(presentable.getSong().getImage())) { PresentationPosition.forSong(presentationPosition) .ifPresent(spp -> { // queue for LATER because the revalidate/repaint above only will run diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java b/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java new file mode 100644 index 00000000..05c7dbbf --- /dev/null +++ b/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java @@ -0,0 +1,124 @@ +package org.zephyrsoft.sdb2.remote; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import org.zephyrsoft.sdb2.FileAndDirectoryLocations; +import org.zephyrsoft.sdb2.model.Song; +import org.zephyrsoft.sdb2.model.SongsModel; +import org.zephyrsoft.sdb2.remote.MqttObject.OnChangeListener; +import org.zephyrsoft.sdb2.util.StringTools; + +public class FileController { + + private RemoteController remoteController; + + public FileController(RemoteController remoteController) { + this.remoteController = remoteController; + this.remoteController.getFilesRequestFile().onRemoteChange((data, args) -> { + try { + Files.createDirectories(Paths.get(FileAndDirectoryLocations.getDBBlobDir())); + } catch (IOException e) { + e.printStackTrace(); + } + try { + Files.write(Paths.get(FileAndDirectoryLocations.getDBBlobDir(), (String) args[0]), data); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private Collection getMissingFiles(Song song){ + HashSet missingFiles = new HashSet<>(); + + if(!StringTools.isEmpty(song.getImage()) && URI.create(song.getImage()).getScheme().equals("sdb")) { + Path path = Paths.get(FileAndDirectoryLocations.getDBBlobDir(), song.getImage().replace("sdb://", "")); + if (!Files.isRegularFile(path)) { + missingFiles.add(path.getFileName().toString()); + } + } + return missingFiles; + } + + public void downloadFiles(Song song, Runnable callback) { + this.downloadFiles(getMissingFiles(song), callback); + } + + public void downloadFiles(SongsModel songsModel, Runnable callback) { + HashSet missingFiles = new HashSet<>(); + + for (Song song: songsModel.getSongs()) { + missingFiles.addAll(getMissingFiles(song)); + } + + this.downloadFiles(missingFiles, callback); + } + + public void downloadFiles(Collection files, Runnable callback) { + if(files.isEmpty()) { + callback.run(); + return; + } + HashSet missingFiles = new HashSet<>(files); + + this.remoteController.getFilesRequestFile().onRemoteChange(new OnChangeListener() { + @Override + public void onChange(byte[] object, Object... args) { + String fileName = (String)args[0]; + if ( missingFiles.contains(fileName)){ + missingFiles.remove(fileName); + if (missingFiles.isEmpty()) { + callback.run(); + remoteController.getFilesRequestFile().removeOnRemoteChangeListener(this); + } + } + } + }); + missingFiles.forEach((fileName) -> remoteController.getFilesRequestGet().set(new FileRequest(fileName))); + } + + public SongsModel uploadFiles(SongsModel songsModel) { + for (Song song: songsModel.getSongs()) { + if(!StringTools.isEmpty(song.getImage()) && URI.create(song.getImage()).getScheme().equals("file")) { + Path path = Paths.get(URI.create(song.getImage())); + String oldFilename = path.getFileName().toString(); + Optional fileExtension = Optional.ofNullable(oldFilename) + .filter(f -> f.contains(".")) + .map(f -> f.substring(oldFilename.lastIndexOf("."))); + String newFilename = StringTools.createUUID() + fileExtension.get(); + Path dbPath = Paths.get(FileAndDirectoryLocations.getDBBlobDir(), newFilename); + try { + Files.createDirectories(Paths.get(FileAndDirectoryLocations.getDBBlobDir())); + } catch (IOException e) { + e.printStackTrace(); + } + try { + Files.copy(path, dbPath, StandardCopyOption.COPY_ATTRIBUTES); + } catch (IOException e) { + e.printStackTrace(); + } + try { + byte[] content = Files.readAllBytes(dbPath); + remoteController.getFilesRequestSet().set(content, newFilename); + } catch (IOException e) { + e.printStackTrace(); + } + song.setImage("sdb://"+newFilename); + } + } + return songsModel; + } + +} diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/FileRequest.java b/src/main/java/org/zephyrsoft/sdb2/remote/FileRequest.java new file mode 100644 index 00000000..04c32a84 --- /dev/null +++ b/src/main/java/org/zephyrsoft/sdb2/remote/FileRequest.java @@ -0,0 +1,49 @@ +/* + * This file is part of the Song Database (SDB). + * + * SDB is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License 3.0 as published by + * the Free Software Foundation. + * + * SDB is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License 3.0 for more details. + * + * You should have received a copy of the GNU General Public License 3.0 + * along with SDB. If not, see . + */ +package org.zephyrsoft.sdb2.remote; + +import org.zephyrsoft.sdb2.model.Persistable; + +import jakarta.xml.bind.annotation.XmlAccessOrder; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorOrder; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "fileRequest") +@XmlAccessorType(XmlAccessType.NONE) +@XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL) +public class FileRequest implements Persistable { + private static final long serialVersionUID = 8867565661007188776L; + + @XmlElement(name = "uuid") + private String uuid; + + public FileRequest() { + initIfNecessary(); + } + + public FileRequest(String uuid) { + this(); + this.uuid = uuid; + } + + @Override + public void initIfNecessary() { + uuid = null; + } +} diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/MQTT.java b/src/main/java/org/zephyrsoft/sdb2/remote/MQTT.java index 68ffa09f..93e649e0 100644 --- a/src/main/java/org/zephyrsoft/sdb2/remote/MQTT.java +++ b/src/main/java/org/zephyrsoft/sdb2/remote/MQTT.java @@ -16,6 +16,7 @@ package org.zephyrsoft.sdb2.remote; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; @@ -99,7 +100,7 @@ public void publish(String topic, byte[] payload, int qos, boolean retained) { LOG.debug("Publishing message: {}", topic); try { client.publish(topic, payload, qos, retained); - LOG.debug("Payload: " + new String(payload, StandardCharsets.UTF_8)); + LOG.debug("Payload: " + new String(Arrays.copyOfRange(payload, 0, 50), StandardCharsets.UTF_8)); } catch (Exception e) { // only log the exception LOG.warn("could not publish message", e); diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java index 10358eb6..24d5289d 100644 --- a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java +++ b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java @@ -17,10 +17,18 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Optional; import org.eclipse.paho.client.mqttv3.MqttException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.zephyrsoft.sdb2.FileAndDirectoryLocations; import org.zephyrsoft.sdb2.MainController; import org.zephyrsoft.sdb2.gui.MainWindow; import org.zephyrsoft.sdb2.model.Persistable; @@ -32,9 +40,9 @@ import org.zephyrsoft.sdb2.util.StringTools; public class RemoteController { - + private static final Logger LOG = LoggerFactory.getLogger(RemoteController.class); - + private final MqttObject song; private final MqttObject position; private final MqttObject playlist; @@ -43,73 +51,78 @@ public class RemoteController { private final MqttObject requestGet; private final MqttObject requestPatches; private final MqttObject latestReject; + private MqttObject filesRequestSet; + private MqttObject filesRequestGet; + private MqttObject filesRequestFile; private final MqttObject healthDB; private final RemotePresenter remotePresenter; private RemotePreferences remotePreferences; - + private FileController fileController; + /** - * Creates a RemoteController instance by connecting to a broker and setting up properties. + * Creates a RemoteController instance by connecting to a broker and setting up + * properties. *

- * You may set a prefix if you want to share a broker with multiple instance groups. - * The prefix may be a code, where only selected users have access too. It must be set if you want to set up or use - * a global mqtt server for multiple organizations. - * A Organization may have different rooms, to split up presentation instances into. - * Users may have only access to some rooms. + * You may set a prefix if you want to share a broker with multiple instance + * groups. The prefix may be a code, where only selected users have access too. + * It must be set if you want to set up or use a global mqtt server for multiple + * organizations. A Organization may have different rooms, to split up + * presentation instances into. Users may have only access to some rooms. *

* A room contains one shared presentation, and multiple playlists. *

- * All properties are by default without local notify. They can't be used for in program synchronization. - * Furthermore they are retained. + * All properties are by default without local notify. They can't be used for in + * program synchronization. Furthermore they are retained. * - * @param mainWindow - * can be null, if headless + * @param mainWindow can be null, if headless */ public RemoteController(RemotePreferences remotePreferences, MainController mainController, MainWindow mainWindow) { this.remotePreferences = remotePreferences; - + position = new MqttObject<>(formatTopic(RemoteTopic.POSITION), (s) -> (Position) parseXML(s), - RemoteController::toXML, RemoteTopic.POSITION_QOS, RemoteTopic.POSITION_RETAINED, - null); + RemoteController::toXML, RemoteTopic.POSITION_QOS, RemoteTopic.POSITION_RETAINED, null); position.onRemoteChange((p, a) -> updateSongOrPosition(mainController, mainWindow)); - - song = new MqttObject<>(formatTopic(RemoteTopic.SONG), - (s) -> (Song) parseXML(s), RemoteController::toXML, RemoteTopic.SONG_QOS, RemoteTopic.SONG_RETAINED, - null); + + song = new MqttObject<>(formatTopic(RemoteTopic.SONG), (s) -> (Song) parseXML(s), RemoteController::toXML, + RemoteTopic.SONG_QOS, RemoteTopic.SONG_RETAINED, null); song.onRemoteChange((s, a) -> updateSongOrPosition(mainController, mainWindow)); - + if (mainWindow != null) { - playlist = new MqttObject<>(formatTopic(RemoteTopic.PLAYLIST), - (s) -> (SongsModel) parseXML(s), - RemoteController::toXML, RemoteTopic.PLAYLIST_QOS, RemoteTopic.PLAYLIST_RETAINED, - null); - playlist.onRemoteChange((p, a) -> mainWindow.updatePlaylist(p)); - mainWindow.getPresentModel().addSongsModelListener(() -> playlist.set(new SongsModel(mainWindow.getPresentModel()))); - + playlist = new MqttObject<>(formatTopic(RemoteTopic.PLAYLIST), (s) -> (SongsModel) parseXML(s), + RemoteController::toXML, RemoteTopic.PLAYLIST_QOS, RemoteTopic.PLAYLIST_RETAINED, null); + playlist.onRemoteChange((p, a) -> fileController.downloadFiles(p, () -> mainWindow.updatePlaylist(p))); + mainWindow.getPresentModel().addSongsModelListener(() -> playlist.set(new SongsModel(fileController.uploadFiles(mainWindow.getPresentModel())))); + latestVersion = new MqttObject<>(formatTopic(RemoteTopic.PATCHES_LATEST_VERSION), - (s) -> (Version) parseXML(s), - RemoteController::toXML, RemoteTopic.PATCHES_LATEST_VERSION_QOS, RemoteTopic.PATCHES_LATEST_VERSION_RETAINED, - (a, b) -> false); - + (s) -> (Version) parseXML(s), RemoteController::toXML, RemoteTopic.PATCHES_LATEST_VERSION_QOS, + RemoteTopic.PATCHES_LATEST_VERSION_RETAINED, (a, b) -> false); + latestChanges = new MqttObject<>(formatTopic(RemoteTopic.PATCHES_LATEST_CHANGES), - (s) -> (SongsModel) parseXML(s), - RemoteController::toXML, RemoteTopic.PATCHES_LATEST_CHANGES_QOS, RemoteTopic.PATCHES_LATEST_CHANEGS_RETAINED, - (a, b) -> false); - + (s) -> (SongsModel) parseXML(s), RemoteController::toXML, RemoteTopic.PATCHES_LATEST_CHANGES_QOS, + RemoteTopic.PATCHES_LATEST_CHANEGS_RETAINED, (a, b) -> false); + latestReject = new MqttObject<>(formatTopic(RemoteTopic.PATCHES_LATEST_REJECT), - (s) -> (ChangeReject) parseXML(s), - null, RemoteTopic.PATCHES_LATEST_REJECT_QOS, RemoteTopic.PATCHES_LATEST_REJECT_RETAINED, - (a, b) -> false); - - requestGet = new MqttObject<>(formatClientIDTopic(RemoteTopic.PATCHES_REQUEST_GET), - RemoteController::toXML, RemoteTopic.PATCHES_REQUEST_GET_QOS, RemoteTopic.PATCHES_REQUEST_GET_RETAINED); - + (s) -> (ChangeReject) parseXML(s), null, RemoteTopic.PATCHES_LATEST_REJECT_QOS, + RemoteTopic.PATCHES_LATEST_REJECT_RETAINED, (a, b) -> false); + + requestGet = new MqttObject<>(formatClientIDTopic(RemoteTopic.PATCHES_REQUEST_GET), RemoteController::toXML, + RemoteTopic.PATCHES_REQUEST_GET_QOS, RemoteTopic.PATCHES_REQUEST_GET_RETAINED); + requestPatches = new MqttObject<>(formatClientIDTopic(RemoteTopic.PATCHES_REQUEST_PATCHES), - (s) -> (Patches) parseXML(s), - RemoteController::toXML, RemoteTopic.PATCHES_REQUEST_PATCHES_QOS, RemoteTopic.PATCHES_REQUEST_PATCHES_RETAINED, - (a, b) -> false); - - healthDB = new MqttObject<>(formatPrefixTopic(RemoteTopic.HEALTH_DB), - Health::valueOfBytes, null, RemoteTopic.HEALTH_DB_QOS, RemoteTopic.HEALTH_DB_RETAINED, null); + (s) -> (Patches) parseXML(s), RemoteController::toXML, RemoteTopic.PATCHES_REQUEST_PATCHES_QOS, + RemoteTopic.PATCHES_REQUEST_PATCHES_RETAINED, (a, b) -> false); + + filesRequestSet = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_SET), (b) -> b, + RemoteTopic.FILES_REQUEST_SET_QOS, RemoteTopic.FILES_REQUEST_SET_RETAINED); + + filesRequestGet = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_GET), + RemoteController::toXML, RemoteTopic.FILES_REQUEST_GET_QOS, RemoteTopic.FILES_REQUEST_GET_RETAINED); + + filesRequestFile = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_FILE), null, null, + RemoteTopic.FILES_REQUEST_FILE_QOS, RemoteTopic.FILES_REQUEST_FILE_RETAINED, (a, b) -> false); + + healthDB = new MqttObject<>(formatPrefixTopic(RemoteTopic.HEALTH_DB), Health::valueOfBytes, null, + RemoteTopic.HEALTH_DB_QOS, RemoteTopic.HEALTH_DB_RETAINED, null); } else { this.playlist = null; this.requestPatches = null; @@ -120,9 +133,10 @@ public RemoteController(RemotePreferences remotePreferences, MainController main this.healthDB = null; } + fileController = new FileController(this); remotePresenter = new RemotePresenter(this); } - + public void connectTo(MQTT mqtt) throws MqttException { song.connectTo(mqtt); position.connectTo(mqtt); @@ -133,8 +147,11 @@ public void connectTo(MQTT mqtt) throws MqttException { requestPatches.connectTo(mqtt); latestReject.connectTo(mqtt); healthDB.connectTo(mqtt); + filesRequestFile.connectTo(mqtt); + filesRequestGet.connectTo(mqtt); + filesRequestSet.connectTo(mqtt); } - + private void updateSongOrPosition(MainController mainController, MainWindow mainWindow) { if (position == null || song == null) return; @@ -147,50 +164,49 @@ private void updateSongOrPosition(MainController mainController, MainWindow main if (s == null) { return; } - + if (StringTools.equals(s.getUUID(), p.getUUID())) { if (p.isVisible()) { + p = StringTools.isEmpty(s.getImage()) ? p : null; presentSong(mainController, mainWindow, s, p); } else { presentSong(mainController, mainWindow, null, null); } } } - + private void presentSong(MainController mainController, MainWindow mainWindow, Song s, Position p) { - SongPresentationPosition spp = p != null - ? new SongPresentationPosition(p.getPart(), p.getLine()) - : null; + SongPresentationPosition spp = p != null ? new SongPresentationPosition(p.getPart(), p.getLine()) : null; if (mainWindow != null) { mainWindow.present(s, spp); } else { mainController.present(new Presentable(s, null), spp); } } - + public MqttObject getSong() { return song; } - + public MqttObject getPlaylist() { return playlist; } - + public MqttObject getPosition() { return position; } - + public RemotePresenter getRemotePresenter(Presentable presentable) { remotePresenter.setContent(presentable, null); return remotePresenter; } - + static Persistable parseXML(byte[] xml) { if (xml.length == 0) return null; return XMLConverter.fromXMLToPersistable(new ByteArrayInputStream(xml)); } - + static byte[] toXML(Persistable persistable) { if (persistable == null) return new byte[0]; @@ -203,47 +219,62 @@ static byte[] toXML(Persistable persistable) { } return null; } - + private String formatTopic(String topic) { - return String.format(topic, getRemotePreferences().getPrefix().isBlank() ? "" : getRemotePreferences().getPrefix() + "/", - getRemotePreferences().getRoom()); + return String.format(topic, + getRemotePreferences().getPrefix().isBlank() ? "" : getRemotePreferences().getPrefix() + "/", + getRemotePreferences().getRoom()); } - + private String formatClientIDTopic(String topic) { - return String.format(topic, getRemotePreferences().getPrefix().isBlank() ? "" : getRemotePreferences().getPrefix() + "/", remotePreferences - .getClientID()); + return String.format(topic, + getRemotePreferences().getPrefix().isBlank() ? "" : getRemotePreferences().getPrefix() + "/", + remotePreferences.getClientID()); } - + private String formatPrefixTopic(String topic) { - return String.format(topic, getRemotePreferences().getPrefix().isEmpty() ? "" : getRemotePreferences().getPrefix() + "/"); + return String.format(topic, + getRemotePreferences().getPrefix().isEmpty() ? "" : getRemotePreferences().getPrefix() + "/"); } - + public MqttObject getLatestVersion() { return latestVersion; } - + public MqttObject getLatestChanges() { return latestChanges; } - + public MqttObject getRequestPatches() { return requestPatches; } - + public MqttObject getRequestGet() { return requestGet; } - + public MqttObject getLatestReject() { return latestReject; } - + public MqttObject getHealthDB() { return healthDB; } - + public RemotePreferences getRemotePreferences() { return remotePreferences; } - + + public MqttObject getFilesRequestSet() { + return filesRequestSet; + } + + public MqttObject getFilesRequestGet() { + return filesRequestGet; + } + + public MqttObject getFilesRequestFile() { + return filesRequestFile; + } + } diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java index 2b2babbb..140df4dc 100644 --- a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java +++ b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java @@ -64,4 +64,21 @@ public class RemoteTopic { final static String PATCHES_REQUEST_PATCHES = "%sdb/v1/patches/request/%s/patches"; final static int PATCHES_REQUEST_PATCHES_QOS = 1; final static boolean PATCHES_REQUEST_PATCHES_RETAINED = false; + + // If client uploads a file: to username/uuid + final static String FILES_REQUEST_SET = "%sdb/v1/files/request/%s/set/+"; + final static int FILES_REQUEST_SET_QOS = 1; + final static boolean FILES_REQUEST_SET_RETAINED = false; + + // If client needs a file, it sends a request to get + final static String FILES_REQUEST_GET = "%sdb/v1/files/request/%s/get"; + final static int FILES_REQUEST_GET_QOS = 1; + final static boolean FILES_REQUEST_GET_RETAINED = false; + + // And it will recieve the requested file to uuid: + final static String FILES_REQUEST_FILE = "%sdb/v1/files/request/%s/file/+"; + final static int FILES_REQUEST_FILE_QOS = 1; + final static boolean FILES_REQUEST_FILE_RETAINED = false; + public final static int FILES_REQUEST_FILE_UUID = 0; + } From da07a91a47dca9f5d1a4bbe81d6e64321f516d41 Mon Sep 17 00:00:00 2001 From: Lenard Spiecker <17047770+l-spiecker@users.noreply.github.com> Date: Wed, 20 Mar 2024 00:36:50 +0100 Subject: [PATCH 2/2] chg: filesync file set response --- .../zephyrsoft/sdb2/model/XMLConverter.java | 3 +- .../sdb2/remote/FileController.java | 46 +++++++++++--- .../sdb2/remote/FileSetResponse.java | 60 +++++++++++++++++++ .../sdb2/remote/RemoteController.java | 24 ++++++-- .../zephyrsoft/sdb2/remote/RemoteTopic.java | 5 ++ 5 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/zephyrsoft/sdb2/remote/FileSetResponse.java diff --git a/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java b/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java index 3b92ad7c..c308e9fa 100644 --- a/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java +++ b/src/main/java/org/zephyrsoft/sdb2/model/XMLConverter.java @@ -22,6 +22,7 @@ import org.zephyrsoft.sdb2.model.statistics.StatisticsModel; import org.zephyrsoft.sdb2.remote.ChangeReject; import org.zephyrsoft.sdb2.remote.FileRequest; +import org.zephyrsoft.sdb2.remote.FileSetResponse; import org.zephyrsoft.sdb2.remote.PatchRequest; import org.zephyrsoft.sdb2.remote.Patches; import org.zephyrsoft.sdb2.remote.Position; @@ -66,7 +67,7 @@ public static T fromXMLToPersistable(InputStream xmlInpu private static JAXBContext createContext() throws JAXBException { JAXBContext context = JAXBContext.newInstance(SongsModel.class, SettingsModel.class, StatisticsModel.class, Version.class, - PatchRequest.class, Song.class, Patches.class, ChangeReject.class, Position.class, FileRequest.class); + PatchRequest.class, Song.class, Patches.class, ChangeReject.class, Position.class, FileRequest.class, FileSetResponse.class); return context; } diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java b/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java index 05c7dbbf..b1fcb3d3 100644 --- a/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java +++ b/src/main/java/org/zephyrsoft/sdb2/remote/FileController.java @@ -89,7 +89,10 @@ public void onChange(byte[] object, Object... args) { missingFiles.forEach((fileName) -> remoteController.getFilesRequestGet().set(new FileRequest(fileName))); } - public SongsModel uploadFiles(SongsModel songsModel) { + + public void uploadFiles(SongsModel songsModel, Consumer callback) { + HashSet localFiles = new HashSet<>(); + for (Song song: songsModel.getSongs()) { if(!StringTools.isEmpty(song.getImage()) && URI.create(song.getImage()).getScheme().equals("file")) { Path path = Paths.get(URI.create(song.getImage())); @@ -98,6 +101,7 @@ public SongsModel uploadFiles(SongsModel songsModel) { .filter(f -> f.contains(".")) .map(f -> f.substring(oldFilename.lastIndexOf("."))); String newFilename = StringTools.createUUID() + fileExtension.get(); + localFiles.add(newFilename); Path dbPath = Paths.get(FileAndDirectoryLocations.getDBBlobDir(), newFilename); try { Files.createDirectories(Paths.get(FileAndDirectoryLocations.getDBBlobDir())); @@ -109,16 +113,42 @@ public SongsModel uploadFiles(SongsModel songsModel) { } catch (IOException e) { e.printStackTrace(); } - try { - byte[] content = Files.readAllBytes(dbPath); - remoteController.getFilesRequestSet().set(content, newFilename); - } catch (IOException e) { - e.printStackTrace(); - } song.setImage("sdb://"+newFilename); } } - return songsModel; + if(localFiles.isEmpty()) { + callback.accept(songsModel); + return; + } + + this.remoteController.getFilesRequestSetResponse().onRemoteChange(new OnChangeListener() { + @Override + public void onChange(FileSetResponse object, Object... args) { + String fileName = object.getUuid(); + if ( localFiles.contains(fileName) ){ + if(!object.isOk()) { + System.err.println("Could not upload file " + fileName + " Reason: " + object.getReason()); + remoteController.getFilesRequestSetResponse().removeOnRemoteChangeListener(this); + }else { + localFiles.remove(fileName); + if (localFiles.isEmpty()) { + callback.accept(songsModel); + remoteController.getFilesRequestSetResponse().removeOnRemoteChangeListener(this); + } + } + } + } + }); + + localFiles.forEach((fileName) -> { + Path dbPath = Paths.get(FileAndDirectoryLocations.getDBBlobDir(), fileName); + try { + byte[] content = Files.readAllBytes(dbPath); + remoteController.getFilesRequestSet().set(content, fileName); + } catch (IOException e) { + e.printStackTrace(); + } + }); } } diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/FileSetResponse.java b/src/main/java/org/zephyrsoft/sdb2/remote/FileSetResponse.java new file mode 100644 index 00000000..fb0f2895 --- /dev/null +++ b/src/main/java/org/zephyrsoft/sdb2/remote/FileSetResponse.java @@ -0,0 +1,60 @@ +/* + * This file is part of the Song Database (SDB). + * + * SDB is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License 3.0 as published by + * the Free Software Foundation. + * + * SDB is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License 3.0 for more details. + * + * You should have received a copy of the GNU General Public License 3.0 + * along with SDB. If not, see . + */ +package org.zephyrsoft.sdb2.remote; + +import org.zephyrsoft.sdb2.model.Persistable; + +import jakarta.xml.bind.annotation.XmlAccessOrder; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorOrder; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "fileSetResponse") +@XmlAccessorType(XmlAccessType.NONE) +@XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL) +public class FileSetResponse implements Persistable { + private static final long serialVersionUID = 8867365661007188776L; + + @XmlElement(name = "uuid") + private String uuid = null; + @XmlElement(name = "ok") + private boolean ok = true; + @XmlElement(name = "reason") + private String reason = ""; + + public FileSetResponse() { + initIfNecessary(); + } + + @Override + public void initIfNecessary() { + // Nothing to do + } + + public boolean isOk() { + return ok; + } + + public String getUuid() { + return uuid; + } + + public String getReason() { + return reason; + } +} diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java index 24d5289d..4043d5e4 100644 --- a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java +++ b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteController.java @@ -53,6 +53,7 @@ public class RemoteController { private final MqttObject latestReject; private MqttObject filesRequestSet; private MqttObject filesRequestGet; + private MqttObject filesRequestSetResponse; private MqttObject filesRequestFile; private final MqttObject healthDB; private final RemotePresenter remotePresenter; @@ -87,11 +88,17 @@ public RemoteController(RemotePreferences remotePreferences, MainController main RemoteTopic.SONG_QOS, RemoteTopic.SONG_RETAINED, null); song.onRemoteChange((s, a) -> updateSongOrPosition(mainController, mainWindow)); + filesRequestGet = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_GET), + RemoteController::toXML, RemoteTopic.FILES_REQUEST_GET_QOS, RemoteTopic.FILES_REQUEST_GET_RETAINED); + + filesRequestFile = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_FILE), null, null, + RemoteTopic.FILES_REQUEST_FILE_QOS, RemoteTopic.FILES_REQUEST_FILE_RETAINED, (a, b) -> false); + if (mainWindow != null) { playlist = new MqttObject<>(formatTopic(RemoteTopic.PLAYLIST), (s) -> (SongsModel) parseXML(s), RemoteController::toXML, RemoteTopic.PLAYLIST_QOS, RemoteTopic.PLAYLIST_RETAINED, null); playlist.onRemoteChange((p, a) -> fileController.downloadFiles(p, () -> mainWindow.updatePlaylist(p))); - mainWindow.getPresentModel().addSongsModelListener(() -> playlist.set(new SongsModel(fileController.uploadFiles(mainWindow.getPresentModel())))); + mainWindow.getPresentModel().addSongsModelListener(() -> fileController.uploadFiles(mainWindow.getPresentModel(), (s) -> playlist.set(new SongsModel(s)))); latestVersion = new MqttObject<>(formatTopic(RemoteTopic.PATCHES_LATEST_VERSION), (s) -> (Version) parseXML(s), RemoteController::toXML, RemoteTopic.PATCHES_LATEST_VERSION_QOS, @@ -115,11 +122,9 @@ public RemoteController(RemotePreferences remotePreferences, MainController main filesRequestSet = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_SET), (b) -> b, RemoteTopic.FILES_REQUEST_SET_QOS, RemoteTopic.FILES_REQUEST_SET_RETAINED); - filesRequestGet = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_GET), - RemoteController::toXML, RemoteTopic.FILES_REQUEST_GET_QOS, RemoteTopic.FILES_REQUEST_GET_RETAINED); - - filesRequestFile = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_FILE), null, null, - RemoteTopic.FILES_REQUEST_FILE_QOS, RemoteTopic.FILES_REQUEST_FILE_RETAINED, (a, b) -> false); + filesRequestSetResponse = new MqttObject<>(formatClientIDTopic(RemoteTopic.FILES_REQUEST_SET_RESPONSE), + (s) -> (FileSetResponse) parseXML(s), null, RemoteTopic.FILES_REQUEST_SET_RESPONSE_QOS, + RemoteTopic.FILES_REQUEST_SET_RESPONSE_RETAINED, (a, b) -> false); healthDB = new MqttObject<>(formatPrefixTopic(RemoteTopic.HEALTH_DB), Health::valueOfBytes, null, RemoteTopic.HEALTH_DB_QOS, RemoteTopic.HEALTH_DB_RETAINED, null); @@ -131,6 +136,8 @@ public RemoteController(RemotePreferences remotePreferences, MainController main this.latestReject = null; this.latestChanges = null; this.healthDB = null; + this.filesRequestSet = null; + this.filesRequestSetResponse = null; } fileController = new FileController(this); @@ -150,6 +157,7 @@ public void connectTo(MQTT mqtt) throws MqttException { filesRequestFile.connectTo(mqtt); filesRequestGet.connectTo(mqtt); filesRequestSet.connectTo(mqtt); + filesRequestSetResponse.connectTo(mqtt); } private void updateSongOrPosition(MainController mainController, MainWindow mainWindow) { @@ -277,4 +285,8 @@ public MqttObject getFilesRequestFile() { return filesRequestFile; } + public MqttObject getFilesRequestSetResponse() { + return filesRequestSetResponse; + } + } diff --git a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java index 140df4dc..38066005 100644 --- a/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java +++ b/src/main/java/org/zephyrsoft/sdb2/remote/RemoteTopic.java @@ -70,6 +70,11 @@ public class RemoteTopic { final static int FILES_REQUEST_SET_QOS = 1; final static boolean FILES_REQUEST_SET_RETAINED = false; + // If client uploads a file, response: to username + final static String FILES_REQUEST_SET_RESPONSE = "%sdb/v1/files/request/%s/set_response"; + final static int FILES_REQUEST_SET_RESPONSE_QOS = 1; + final static boolean FILES_REQUEST_SET_RESPONSE_RETAINED = false; + // If client needs a file, it sends a request to get final static String FILES_REQUEST_GET = "%sdb/v1/files/request/%s/get"; final static int FILES_REQUEST_GET_QOS = 1;