diff options
author | Simon Rettberg | 2015-08-21 20:26:09 +0200 |
---|---|---|
committer | Simon Rettberg | 2015-08-21 20:26:09 +0200 |
commit | 1f8d2edf172c35b2396e568383d9e461c058a1fa (patch) | |
tree | 90662568b358a6c06df5533fbf67a236ed85fda3 /dozentenmodulserver/src/main/java | |
parent | [client] tried to fix teh broken (diff) | |
download | tutor-module-1f8d2edf172c35b2396e568383d9e461c058a1fa.tar.gz tutor-module-1f8d2edf172c35b2396e568383d9e461c058a1fa.tar.xz tutor-module-1f8d2edf172c35b2396e568383d9e461c058a1fa.zip |
[server] Implement missing lecture thrift calls, clean up transfer handling a bit
Diffstat (limited to 'dozentenmodulserver/src/main/java')
12 files changed, 444 insertions, 133 deletions
diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbImage.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbImage.java index a6b6d4b8..5a002e76 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbImage.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbImage.java @@ -1,5 +1,6 @@ package org.openslx.bwlp.sat.database.mappers; +import java.io.File; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -12,6 +13,7 @@ import org.openslx.bwlp.sat.database.MysqlConnection; import org.openslx.bwlp.sat.database.MysqlStatement; import org.openslx.bwlp.sat.database.Paginator; import org.openslx.bwlp.sat.database.models.LocalImageVersion; +import org.openslx.bwlp.sat.fileserv.FileServer; import org.openslx.bwlp.sat.permissions.User; import org.openslx.bwlp.thrift.iface.ImageBaseWrite; import org.openslx.bwlp.thrift.iface.ImageDetailsRead; @@ -108,20 +110,36 @@ public class DbImage { public static LocalImageVersion getLocalImageData(String imageVersionId) throws TNotFoundException, SQLException { try (MysqlConnection connection = Database.getConnection()) { - MysqlStatement stmt = connection.prepareStatement("SELECT imageversionid, imagebaseid, filepath, filesize" - + " FROM imageversion WHERE imageversionid = :versionid LIMIT 1"); - stmt.setString("versionid", imageVersionId); + MysqlStatement stmt = connection.prepareStatement("SELECT imageversionid, imagebaseid, filepath, filesize, createtime, isvalid" + + " FROM imageversion WHERE imageversionid = :imageversionid"); + stmt.setString("imageversionid", imageVersionId); ResultSet rs = stmt.executeQuery(); if (!rs.next()) throw new TNotFoundException(); return new LocalImageVersion(rs.getString("imageversionid"), rs.getString("imagebaseid"), - rs.getString("filepath"), rs.getLong("filesize")); + rs.getString("filepath"), rs.getLong("filesize"), rs.getLong("createtime"), + rs.getBoolean("isvalid")); } catch (SQLException e) { LOGGER.error("Query failed in DbImage.getLocalImageData()", e); throw e; } } + protected static List<LocalImageVersion> getLocalImageVersions(MysqlConnection connection, + String imageBaseId) throws SQLException { + MysqlStatement stmt = connection.prepareStatement("SELECT imageversionid, imagebaseid, filepath, filesize, createtime, isvalid" + + " FROM imageversion WHERE imagebaseid = :imagebaseid"); + stmt.setString("imagebaseid", imageBaseId); + ResultSet rs = stmt.executeQuery(); + List<LocalImageVersion> list = new ArrayList<>(); + while (rs.next()) { + list.add(new LocalImageVersion(rs.getString("imageversionid"), rs.getString("imagebaseid"), + rs.getString("filepath"), rs.getLong("filesize"), rs.getLong("createtime"), + rs.getBoolean("isvalid"))); + } + return list; + } + /** * Private helper to create an {@link ImageSummaryRead} instance from a * {@link ResultSet} @@ -456,13 +474,18 @@ public class DbImage { } } + public static void markValid(MysqlConnection connection, LocalImageVersion imageVersion, boolean valid) + throws SQLException { + MysqlStatement stmt = connection.prepareStatement("UPDATE imageversion SET isvalid = :valid" + + " WHERE imageversionid = :imageversionid"); + stmt.setString("imageversionid", imageVersion.imageVersionId); + stmt.setBoolean("valid", valid); + stmt.executeUpdate(); + } + public static void markValid(LocalImageVersion imageVersion, boolean valid) throws SQLException { try (MysqlConnection connection = Database.getConnection()) { - MysqlStatement stmt = connection.prepareStatement("UPDATE imageversion SET isvalid = :valid" - + " WHERE imageversionid = :imageversionid"); - stmt.setString("imageversionid", imageVersion.imageVersionId); - stmt.setBoolean("valid", valid); - stmt.executeUpdate(); + markValid(connection, imageVersion, valid); connection.commit(); } catch (SQLException e) { LOGGER.error("Query failed in DbImage.markInvalid()", e); @@ -505,15 +528,20 @@ public class DbImage { throw new TNotFoundException(); } // Determine new latest version, as we might have to update the imagebase and lecture tables - List<ImageVersionDetails> versions = DbImage.getImageVersions(connection, imageBaseId); - ImageVersionDetails newVersion = null; - ImageVersionDetails oldVersion = null; - for (ImageVersionDetails version : versions) { - if (version.versionId.equals(changingImageVersionId)) { + List<LocalImageVersion> versions = DbImage.getLocalImageVersions(connection, imageBaseId); + LocalImageVersion newVersion = null; + LocalImageVersion oldVersion = null; + for (LocalImageVersion version : versions) { + if (version.imageVersionId.equals(changingImageVersionId)) { oldVersion = version; } if (version.isValid && (newVersion == null || version.createTime > newVersion.createTime)) { - newVersion = version; + File versionFile = FileServer.composeAbsolutePath(version); + if (versionFile.canRead() && versionFile.length() == version.fileSize) { + newVersion = version; + } else { + markValid(connection, version, false); + } } } if (oldVersion == null) { @@ -521,7 +549,7 @@ public class DbImage { } else { // Switch any lectures linking to this version if applicable if (oldVersion.isValid) { - DbLecture.autoUpdateUsedImage(connection, imageBaseId, newVersion.versionId); + DbLecture.autoUpdateUsedImage(connection, imageBaseId, newVersion.imageVersionId); } else { DbLecture.forcefullySwitchUsedImage(connection, oldVersion, newVersion); } @@ -529,7 +557,7 @@ public class DbImage { // Now update the latestversionid of the baseimage if applicable MysqlStatement latestStmt = connection.prepareStatement("UPDATE imagebase SET latestversionid = :newversionid" + " WHERE imagebaseid = :imagebaseid"); - latestStmt.setString("newversionid", newVersion == null ? null : newVersion.versionId); + latestStmt.setString("newversionid", newVersion == null ? null : newVersion.imageVersionId); latestStmt.setString("imagebaseid", imageBaseId); latestStmt.executeUpdate(); } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecture.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecture.java index 580c19c9..bfd1513a 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecture.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecture.java @@ -11,9 +11,9 @@ import org.apache.log4j.Logger; import org.openslx.bwlp.sat.database.Database; import org.openslx.bwlp.sat.database.MysqlConnection; import org.openslx.bwlp.sat.database.MysqlStatement; +import org.openslx.bwlp.sat.database.models.LocalImageVersion; import org.openslx.bwlp.sat.permissions.User; import org.openslx.bwlp.sat.util.Json; -import org.openslx.bwlp.thrift.iface.ImageVersionDetails; import org.openslx.bwlp.thrift.iface.LectureRead; import org.openslx.bwlp.thrift.iface.LectureSummary; import org.openslx.bwlp.thrift.iface.LectureWrite; @@ -138,7 +138,7 @@ public class DbLecture { + " l.isexam, l.hasinternetaccess, l.caneditdefault, l.canadmindefault," + " perm.canedit, perm.canadmin FROM lecture l" + " LEFT JOIN lecturepermission perm ON (perm.lectureid = l.lectureid AND perm.userid = :userid)" - + " WHERE lectureid = :lectureid"); + + " WHERE l.lectureid = :lectureid"); stmt.setString("lectureid", lectureId); stmt.setString("userid", user.userId); ResultSet rs = stmt.executeQuery(); @@ -336,13 +336,13 @@ public class DbLecture { * Called when an image version is deleted or marked for deletion, so that * linking lectures switch over to other available versions. */ - protected static void forcefullySwitchUsedImage(MysqlConnection connection, ImageVersionDetails oldVersion, - ImageVersionDetails newVersion) throws TNotFoundException, SQLException { + protected static void forcefullySwitchUsedImage(MysqlConnection connection, LocalImageVersion oldVersion, + LocalImageVersion newVersion) throws TNotFoundException, SQLException { if (oldVersion == newVersion - || (newVersion != null && newVersion.versionId.equals(oldVersion.versionId))) + || (newVersion != null && newVersion.imageVersionId.equals(oldVersion.imageVersionId))) return; // First, get list of lectures using the image version to switch away from - List<String> lectures = getAllUsingImageVersion(connection, oldVersion.versionId); + List<String> lectures = getAllUsingImageVersion(connection, oldVersion.imageVersionId); if (lectures.isEmpty()) return; // TODO: If there is no new candidate to switch to, send a warning via mail @@ -353,8 +353,8 @@ public class DbLecture { // Update and send info mail MysqlStatement stmt = connection.prepareStatement("UPDATE lecture SET imageversionid = :newversionid" + " WHERE imageversionid = :oldversionid"); - stmt.setString("oldversionid", oldVersion.versionId); - stmt.setString("newversionid", newVersion.versionId); + stmt.setString("oldversionid", oldVersion.imageVersionId); + stmt.setString("newversionid", newVersion.imageVersionId); stmt.executeUpdate(); // TODO: Send mails .. Something.sendLectureChangeNotify(lectures, oldVersion, newVersion); } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecturePermissions.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecturePermissions.java index e8bd8a07..77c7ea0d 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecturePermissions.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbLecturePermissions.java @@ -2,11 +2,19 @@ package org.openslx.bwlp.sat.database.mappers; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import org.apache.log4j.Logger; +import org.openslx.bwlp.sat.database.Database; +import org.openslx.bwlp.sat.database.MysqlConnection; +import org.openslx.bwlp.sat.database.MysqlStatement; import org.openslx.bwlp.thrift.iface.LecturePermissions; public class DbLecturePermissions { + private static final Logger LOGGER = Logger.getLogger(DbLecturePermissions.class); + /** * Build an instance of {@link LecturePermissions} by reading the given * columns from the given {@link ResultSet}. If there are no permissions @@ -60,4 +68,51 @@ public class DbLecturePermissions { return fromResultSet(rs, "caneditdefault", "canadmindefault"); } + public static void writeForLecture(String lectureId, Map<String, LecturePermissions> permissions) + throws SQLException { + try (MysqlConnection connection = Database.getConnection()) { + MysqlStatement stmt = connection.prepareStatement("DELETE FROM lecturepermission" + + " WHERE lectureid = :lectureid"); + stmt.setString("lectureid", lectureId); + stmt.executeUpdate(); + stmt = connection.prepareStatement("INSERT INTO lecturepermission" + + " (lectureid, userid, canedit, canadmin)" + + " VALUES (:lectureid, :userid, :canedit, :canadmin)"); + stmt.setString("lectureid", lectureId); + for (Map.Entry<String, LecturePermissions> entry : permissions.entrySet()) { + LecturePermissions perm = entry.getValue(); + stmt.setString("userid", entry.getKey()); + stmt.setBoolean("canedit", perm.edit); + stmt.setBoolean("canadmin", perm.admin); + stmt.executeUpdate(); + } + connection.commit(); + } catch (SQLException e) { + LOGGER.error("Query failed in DbLecturePermissions.writeForLecture()", e); + throw e; + } + } + + public static Map<String, LecturePermissions> getForLecture(String lectureId, boolean adminOnly) + throws SQLException { + try (MysqlConnection connection = Database.getConnection()) { + MysqlStatement stmt = connection.prepareStatement("SELECT userid, canedit, canadmin" + + " FROM lecturepermission WHERE lectureid = :lectureid"); + stmt.setString("lectureid", lectureId); + ResultSet rs = stmt.executeQuery(); + Map<String, LecturePermissions> list = new HashMap<>(); + while (rs.next()) { + boolean admin = rs.getBoolean("canadmin"); + if (adminOnly && !admin) + continue; + LecturePermissions perm = new LecturePermissions(rs.getBoolean("canedit"), admin); + list.put(rs.getString("userid"), perm); + } + return list; + } catch (SQLException e) { + LOGGER.error("Query failed in DbImagePermissions.getForImageBase()", e); + throw e; + } + } + } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbUser.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbUser.java index 886f08ec..9c80caf4 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbUser.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/mappers/DbUser.java @@ -92,7 +92,8 @@ public class DbUser { + " (userid, firstname, lastname, email, organizationid, lastlogin, canlogin, issuperuser)" + " VALUES" + " (:userid, :firstname, :lastname, :email, :organizationid, UNIX_TIMESTAMP(), 1, 0)" - + " ON DUPLICATE KEY UPDATE lastlogin = UNIX_TIMESTAMP()"); + + " ON DUPLICATE KEY UPDATE lastlogin = UNIX_TIMESTAMP(), email = VALUES(email)," + + " firstname = VALUES(firstname), lastname = VALUES(lastname)"); stmt.setString("userid", ui.userId); stmt.setString("firstname", ui.firstName); stmt.setString("lastname", ui.lastName); diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/models/LocalImageVersion.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/models/LocalImageVersion.java index 91c5acac..875c46f3 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/models/LocalImageVersion.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/database/models/LocalImageVersion.java @@ -9,12 +9,19 @@ public class LocalImageVersion { public final String filePath; public final long fileSize; - - public LocalImageVersion(String imageVersionId, String imageBaseId, String filePath, long fileSize) { + + public final boolean isValid; + + public final long createTime; + + public LocalImageVersion(String imageVersionId, String imageBaseId, String filePath, long fileSize, + long createTime, boolean isValid) { this.imageVersionId = imageVersionId; this.imageBaseId = imageBaseId; this.filePath = filePath; this.fileSize = fileSize; + this.createTime = createTime; + this.isValid = isValid; } } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/AbstractTransfer.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/AbstractTransfer.java new file mode 100644 index 00000000..bef4f8fd --- /dev/null +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/AbstractTransfer.java @@ -0,0 +1,92 @@ +package org.openslx.bwlp.sat.fileserv; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public abstract class AbstractTransfer { + + /** + * How long to keep this transfer information when the transfer is + * (potentially) done + */ + private static final long FINISH_TIMEOUT = TimeUnit.MINUTES.toMillis(5); + + /** + * How long to keep this transfer information when there are no active + * connections and the transfer seems unfinished + */ + private static final long IDLE_TIMEOUT = TimeUnit.HOURS.toMillis(4); + + /** + * Time stamp of when (we think) the transfer finished. Clients can/might + * not tell us they're done, and simply taking "no active connection" as a + * sign the download is done might have unwanted effects if the user's + * connection drops for a minute. If this time stamp (plus a FINISH_TIMEOUT) + * passed, + * we consider the download done and flag it for removal. + * If set to zero, the transfer is not finished, or not assumed to have + * finished. + */ + protected final AtomicLong potentialFinishTime = new AtomicLong(0); + + /** + * Time of last activity on this transfer. + */ + protected final AtomicLong lastActivityTime = new AtomicLong(System.currentTimeMillis()); + + private final String transferId; + + public AbstractTransfer(String transferId) { + this.transferId = transferId; + } + + /** + * Returns true if the transfer is considered completed. + * + * @param now pass System.currentTimeMillis() + * @return true if the transfer is considered completed + */ + public boolean isComplete(long now) { + long val = potentialFinishTime.get(); + return val != 0 && val + FINISH_TIMEOUT < now; + } + + /** + * Returns true if there has been no activity on this transfer for a certain + * amount of time. + * + * @param now pass System.currentTimeMillis() + * @return true if the transfer reached its idle timeout + */ + public final boolean hasReachedIdleTimeout(long now) { + return getActiveConnectionCount() == 0 && lastActivityTime.get() + IDLE_TIMEOUT < now; + } + + public final String getId() { + return transferId; + } + + /** + * Returns true if this transfer would potentially accept new connections. + * This should NOT return false if there are too many concurrent + * connections, as this is used to signal the client whether to keep trying + * to connect. + * + * @return true if this transfer would potentially accept new connections + */ + public abstract boolean isActive(); + + /** + * Cancel this transfer, aborting all active connections and rejecting + * further incoming ones. + */ + public abstract void cancel(); + + /** + * Returns number of active transfer connections. + * + * @return number of active transfer connections + */ + public abstract int getActiveConnectionCount(); + +} diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveDownload.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveDownload.java index 7180fe3d..504317f3 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveDownload.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveDownload.java @@ -7,43 +7,29 @@ import java.util.concurrent.ThreadPoolExecutor; import org.apache.log4j.Logger; import org.openslx.bwlp.sat.util.Constants; -import org.openslx.bwlp.thrift.iface.TransferState; import org.openslx.filetransfer.Uploader; -public class ActiveDownload { +public class ActiveDownload extends AbstractTransfer { private static final Logger LOGGER = Logger.getLogger(ActiveDownload.class); /** - * How many concurrent connections per upload + * How many concurrent connections per download */ private static final int MAX_CONNECTIONS = Math.max(Constants.MAX_DOWNLOADS / 4, 1); /** - * Self reference for inner classes. - */ - private final ActiveDownload activeDownload = this; - - /** * This is a download, so we have uploaders */ private List<Uploader> uploads = new ArrayList<>(); private final File sourceFile; - private final long fileSize; - - private final String transferId; - - /** - * TransferState of this upload - */ - private TransferState state = TransferState.IDLE; + private boolean isCanceled = false; public ActiveDownload(String uuid, File file) { - this.transferId = uuid; + super(uuid); this.sourceFile = file; - this.fileSize = file.length(); } /** @@ -55,6 +41,9 @@ public class ActiveDownload { * discarded */ public synchronized boolean addConnection(final Uploader connection, ThreadPoolExecutor pool) { + if (isCanceled) + return false; + potentialFinishTime.set(0); synchronized (uploads) { if (uploads.size() > MAX_CONNECTIONS) return false; @@ -64,7 +53,15 @@ public class ActiveDownload { pool.execute(new Runnable() { @Override public void run() { - connection.upload(sourceFile.getAbsolutePath()); + potentialFinishTime.set(0); + boolean ret = connection.upload(sourceFile.getAbsolutePath()); + synchronized (uploads) { + uploads.remove(connection); + } + if (ret && uploads.isEmpty()) { + potentialFinishTime.set(System.currentTimeMillis()); + } + lastActivityTime.set(System.currentTimeMillis()); } }); } catch (Exception e) { @@ -74,13 +71,24 @@ public class ActiveDownload { return true; } - public boolean isComplete() { - // TODO Auto-generated method stub - return false; + @Override + public synchronized void cancel() { + isCanceled = true; + synchronized (uploads) { + for (Uploader u : uploads) { + u.cancel(); + } + } + } + + @Override + public boolean isActive() { + return !isCanceled; } - public String getId() { - return transferId; + @Override + public int getActiveConnectionCount() { + return uploads.size(); } } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveUpload.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveUpload.java index b3bf4133..16f85fc1 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveUpload.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/ActiveUpload.java @@ -33,7 +33,7 @@ import org.openslx.filetransfer.util.HashChecker; import org.openslx.filetransfer.util.HashChecker.HashCheckCallback; import org.openslx.filetransfer.util.HashChecker.HashResult; -public class ActiveUpload implements HashCheckCallback { +public class ActiveUpload extends AbstractTransfer implements HashCheckCallback { private static final Logger LOGGER = Logger.getLogger(ActiveUpload.class); @@ -52,9 +52,9 @@ public class ActiveUpload implements HashCheckCallback { */ private List<Downloader> downloads = new ArrayList<>(); - private final File destinationFile; + private final File tmpFileName; - private final RandomAccessFile outFile; + private final RandomAccessFile tmpFileHandle; private final ChunkList chunks; @@ -81,11 +81,6 @@ public class ActiveUpload implements HashCheckCallback { private TransferState state = TransferState.IDLE; /** - * ID of this upload - will become this version id on success. - */ - private final String uploadId; - - /** * Description of this VM - binary dump of e.g. the *.vmx file (VMware) */ private final byte[] machineDescription; @@ -115,13 +110,13 @@ public class ActiveUpload implements HashCheckCallback { public ActiveUpload(String uploadId, UserInfo owner, ImageDetailsRead image, File destinationFile, long fileSize, List<byte[]> sha1Sums, byte[] machineDescription) throws FileNotFoundException { - this.destinationFile = destinationFile; - this.outFile = new RandomAccessFile(destinationFile, "rw"); + super(uploadId); + this.tmpFileName = destinationFile; + this.tmpFileHandle = new RandomAccessFile(destinationFile, "rw"); this.chunks = new ChunkList(fileSize, hashChecker == null ? null : sha1Sums); this.owner = owner; this.image = image; this.fileSize = fileSize; - this.uploadId = uploadId; this.machineDescription = machineDescription; } @@ -160,7 +155,6 @@ public class ActiveUpload implements HashCheckCallback { if (downloads.size() >= MAX_CONNECTIONS) return false; downloads.add(connection); - // TODO: Remove when finished or executor rejects... } try { pool.execute(new Runnable() { @@ -173,14 +167,20 @@ public class ActiveUpload implements HashCheckCallback { // the queue, so it will be handled again later... chunks.markFailed(cbh.currentChunk); } - LOGGER.warn("Download of " + destinationFile.getAbsolutePath() + " failed"); + LOGGER.warn("Download of " + tmpFileName.getAbsolutePath() + " failed"); + } + lastActivityTime.set(System.currentTimeMillis()); + synchronized (downloads) { + downloads.remove(connection); } if (chunks.isComplete()) { finishUpload(); } } }); - state = TransferState.WORKING; + if (state == TransferState.IDLE) { + state = TransferState.WORKING; + } } catch (Exception e) { LOGGER.warn("threadpool rejected the incoming file transfer", e); return false; @@ -189,8 +189,8 @@ public class ActiveUpload implements HashCheckCallback { } /** - * Write some data to the local file. Thread safe so we could - * have multiple concurrent connections later. + * Write some data to the local file. Thread safe so we can + * have multiple concurrent connections. * * @param fileOffset * @param dataLength @@ -200,14 +200,15 @@ public class ActiveUpload implements HashCheckCallback { private void writeFileData(long fileOffset, int dataLength, byte[] data) { if (state != TransferState.WORKING) throw new IllegalStateException("Cannot write to file if state != RUNNING"); - synchronized (outFile) { + synchronized (tmpFileHandle) { try { - outFile.seek(fileOffset); - outFile.write(data, 0, dataLength); + tmpFileHandle.seek(fileOffset); + tmpFileHandle.write(data, 0, dataLength); } catch (IOException e) { - LOGGER.error("Cannot write to '" + destinationFile + LOGGER.error("Cannot write to '" + tmpFileName + "'. Disk full, network storage error, bad permissions, ...?", e); fileWritable = false; + state = TransferState.ERROR; } } } @@ -216,22 +217,26 @@ public class ActiveUpload implements HashCheckCallback { * Called when the upload finished. */ private synchronized void finishUpload() { - synchronized (outFile) { + synchronized (tmpFileHandle) { if (state != TransferState.WORKING) return; - Util.safeClose(outFile); + Util.safeClose(tmpFileHandle); state = TransferState.FINISHED; } + potentialFinishTime.set(System.currentTimeMillis()); + // If owner is not set, this was a repair-transfer, which downloads directly to the existing target file. + // Nothing more to do in that case. + if (isRepairUpload()) + return; LOGGER.info("Finalizing uploaded image " + image.imageName); - File file = destinationFile; // Ready to go. First step: Rename temp file to something usable - File destination = new File(file.getParent(), Formatter.vmName(owner, image.imageName)); + File destination = new File(tmpFileName.getParent(), Formatter.vmName(owner, image.imageName)); // Sanity check: destination should be a sub directory of the vmStorePath String relPath = FileSystem.getRelativePath(destination, Configuration.getVmStoreBasePath()); if (relPath == null) { LOGGER.warn(destination.getAbsolutePath() + " is not a subdir of " + Configuration.getVmStoreBasePath().getAbsolutePath()); - state = TransferState.ERROR; + cancel(); return; } if (relPath.length() > 200) { @@ -242,7 +247,7 @@ public class ActiveUpload implements HashCheckCallback { boolean ret = false; Exception renameException = null; try { - ret = file.renameTo(destination); + ret = tmpFileName.renameTo(destination); } catch (Exception e) { ret = false; renameException = e; @@ -250,35 +255,46 @@ public class ActiveUpload implements HashCheckCallback { if (!ret) { // Rename failed :-( LOGGER.warn( - "Could not rename '" + file.getAbsolutePath() + "' to '" + destination.getAbsolutePath() - + "'", renameException); - state = TransferState.ERROR; + "Could not rename '" + tmpFileName.getAbsolutePath() + "' to '" + + destination.getAbsolutePath() + "'", renameException); + cancel(); return; } // Now insert meta data into DB try { synchronized (versionWrittenToDb) { - DbImage.createImageVersion(image.imageBaseId, uploadId, owner, fileSize, relPath, + DbImage.createImageVersion(image.imageBaseId, getId(), owner, fileSize, relPath, versionSettings, chunks, machineDescription); versionWrittenToDb.set(true); } } catch (SQLException e) { LOGGER.error("Error finishing upload: Inserting version to DB failed", e); state = TransferState.ERROR; + // Also delete uploaded file, as there is no refence to it + FileSystem.deleteAsync(destination); + cancel(); return; } } + @Override public synchronized void cancel() { + if (state != TransferState.FINISHED) { + state = TransferState.ERROR; + if (!isRepairUpload() && tmpFileName.exists()) { + FileSystem.deleteAsync(tmpFileName); + } + } synchronized (downloads) { for (Downloader download : downloads) { download.cancel(); } } - if (state != TransferState.FINISHED) { - state = TransferState.ERROR; - } + } + + public boolean isRepairUpload() { + return owner == null; } /** @@ -290,12 +306,8 @@ public class ActiveUpload implements HashCheckCallback { return this.owner; } - public synchronized boolean isComplete() { - return state == TransferState.FINISHED; - } - public File getDestinationFile() { - return this.destinationFile; + return this.tmpFileName; } public long getSize() { @@ -346,7 +358,6 @@ public class ActiveUpload implements HashCheckCallback { Thread.currentThread().interrupt(); return null; } - LOGGER.info("Next missing chunk: " + currentChunk); if (currentChunk == null) { return null; // No more chunks, returning null tells the Downloader we're done. } @@ -358,10 +369,6 @@ public class ActiveUpload implements HashCheckCallback { return new TransferStatus(chunks.getStatusArray(), state); } - public String getId() { - return uploadId; - } - @Override public void hashCheckDone(HashResult result, byte[] data, FileChunk chunk) { switch (result) { @@ -385,18 +392,18 @@ public class ActiveUpload implements HashCheckCallback { } private byte[] loadChunkFromFile(FileChunk chunk) { - synchronized (outFile) { + synchronized (tmpFileHandle) { if (state != TransferState.IDLE && state != TransferState.WORKING) return null; try { - outFile.seek(chunk.range.startOffset); + tmpFileHandle.seek(chunk.range.startOffset); byte[] buffer = new byte[chunk.range.getLength()]; - outFile.readFully(buffer); + tmpFileHandle.readFully(buffer); return buffer; } catch (IOException e) { LOGGER.error( "Could not read chunk " + chunk.getChunkIndex() + " of File " - + destinationFile.toString(), e); + + tmpFileName.toString(), e); return null; } } @@ -425,6 +432,14 @@ public class ActiveUpload implements HashCheckCallback { } } - // TODO: Clean up old stale uploads + @Override + public boolean isActive() { + return state == TransferState.IDLE || state == TransferState.WORKING; + } + + @Override + public int getActiveConnectionCount() { + return downloads.size(); + } } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/FileServer.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/FileServer.java index a71b7b3f..bfa5699f 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/FileServer.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/fileserv/FileServer.java @@ -115,22 +115,30 @@ public class FileServer implements IncomingEvent { return uploads.get(uploadToken); } + public ActiveDownload getDownloadByToken(String downloadToken) { + if (downloadToken == null) + return null; + return downloads.get(downloadToken); + } + public ActiveUpload createNewUserUpload(UserInfo owner, ImageDetailsRead image, long fileSize, List<byte[]> sha1Sums, byte[] machineDescription) throws TTransferRejectedException { Iterator<ActiveUpload> it = uploads.values().iterator(); + final long now = System.currentTimeMillis(); int activeUploads = 0; while (it.hasNext()) { ActiveUpload upload = it.next(); - if (upload.isComplete()) { - // TODO: Check age (short timeout) and remove + if (upload.isComplete(now) || upload.hasReachedIdleTimeout(now)) { + upload.cancel(); + it.remove(); continue; - } else { - // Check age (long timeout) and remove } activeUploads++; } - if (activeUploads > Constants.MAX_UPLOADS) - throw new TTransferRejectedException("Server busy. Too many running uploads."); + if (activeUploads > Constants.MAX_UPLOADS) { + throw new TTransferRejectedException("Server busy. Too many running uploads (" + activeUploads + + "/" + Constants.MAX_UPLOADS + ")."); + } File destinationFile = null; do { destinationFile = Formatter.getTempImageName(); @@ -152,6 +160,8 @@ public class FileServer implements IncomingEvent { } public int getPlainPort() { + if (plainListener == null) + return 0; return plainListener.getPort(); } @@ -164,21 +174,23 @@ public class FileServer implements IncomingEvent { public ActiveDownload createNewUserDownload(LocalImageVersion localImageData) throws TTransferRejectedException { Iterator<ActiveDownload> it = downloads.values().iterator(); + final long now = System.currentTimeMillis(); int activeDownloads = 0; while (it.hasNext()) { ActiveDownload download = it.next(); - if (download.isComplete()) { - // TODO: Check age (short timeout) and remove + if (download.isComplete(now) || download.hasReachedIdleTimeout(now)) { + download.cancel(); + it.remove(); continue; - } else { - // Check age (long timeout) and remove } activeDownloads++; } - if (activeDownloads > Constants.MAX_DOWNLOADS) - throw new TTransferRejectedException("Server busy. Too many running downloads."); + if (activeDownloads > Constants.MAX_DOWNLOADS) { + throw new TTransferRejectedException("Server busy. Too many running uploads (" + activeDownloads + + "/" + Constants.MAX_UPLOADS + ")."); + } // Determine src file and go - File srcFile = new File(Configuration.getVmStoreBasePath(), localImageData.filePath); + File srcFile = composeAbsolutePath(localImageData); if (!srcFile.canRead()) { LOGGER.warn("Rejecting download of VID " + localImageData.imageVersionId + ": Missing " + srcFile.getPath()); @@ -204,4 +216,8 @@ public class FileServer implements IncomingEvent { return transfer; } + public static File composeAbsolutePath(LocalImageVersion localImageData) { + return new File(Configuration.getVmStoreBasePath(), localImageData.filePath); + } + } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/permissions/User.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/permissions/User.java index 6e5e4d32..e2c86d80 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/permissions/User.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/permissions/User.java @@ -16,6 +16,7 @@ import org.openslx.bwlp.thrift.iface.LecturePermissions; import org.openslx.bwlp.thrift.iface.LectureRead; import org.openslx.bwlp.thrift.iface.LectureSummary; import org.openslx.bwlp.thrift.iface.Role; +import org.openslx.bwlp.thrift.iface.ShareMode; import org.openslx.bwlp.thrift.iface.TAuthorizationException; import org.openslx.bwlp.thrift.iface.TInternalServerError; import org.openslx.bwlp.thrift.iface.TNotFoundException; @@ -153,6 +154,13 @@ public class User { } catch (SQLException e) { throw new TInternalServerError(); } + // Do not allow deleting remote images if share mode is set to "auto download" and + // the version to delete is the latest + if (imageDetails.shareMode == ShareMode.DOWNLOAD + && imageDetails.latestVersionId.equals(imageVersionId)) { + throw new TAuthorizationException(AuthorizationError.NO_PERMISSION, + "Cannot delete latest version of image if auto-download is enabled"); + } // Check user permissions if (imageDetails.userPermissions.admin) return; @@ -167,6 +175,14 @@ public class User { "No permission to delete this image version"); } + public static void canDeleteImageOrFail(ImageDetailsRead imageDetails) throws TAuthorizationException { + // Check user permissions + if (imageDetails.userPermissions.admin) + return; + throw new TAuthorizationException(AuthorizationError.NO_PERMISSION, + "No permission to delete this image"); + } + public static void canDownloadImageVersionOrFail(UserInfo user, String imageVersionId) throws TAuthorizationException, TNotFoundException, TInternalServerError { ImageDetailsRead image; @@ -253,6 +269,31 @@ public class User { } } + public static boolean canEditLecturePermissions(UserInfo user, String lectureId) + throws TNotFoundException, TInternalServerError { + LectureSummary lecture = getLectureFromId(user, lectureId); + return lecture.userPermissions.admin; + } + + public static void canEditLecturePermissionsOrFail(UserInfo user, String lectureId) + throws TAuthorizationException, TNotFoundException, TInternalServerError { + if (!canEditLecturePermissions(user, lectureId)) { + throw new TAuthorizationException(AuthorizationError.NO_PERMISSION, + "No permission to edit permissions"); + } + } + + public static void canChangeLectureOwnerOrFail(UserInfo user, String lectureId) + throws TAuthorizationException, TNotFoundException, TInternalServerError { + // TODO: Who should be allowed to change the owner? Any admin, or just the owner? + // Currently it's every admin, but this is open for discussion + LectureSummary lecture = getLectureFromId(user, lectureId); + if (!lecture.userPermissions.admin) { + throw new TAuthorizationException(AuthorizationError.NO_PERMISSION, + "No permission to change lecture owner"); + } + } + public static boolean canListImages(UserInfo user) throws TAuthorizationException { return isTutor(user); } diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/thrift/ServerHandler.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/thrift/ServerHandler.java index b3e6208e..994466dd 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/thrift/ServerHandler.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/thrift/ServerHandler.java @@ -12,6 +12,7 @@ import org.openslx.bwlp.sat.RuntimeConfig; import org.openslx.bwlp.sat.database.mappers.DbImage; import org.openslx.bwlp.sat.database.mappers.DbImagePermissions; import org.openslx.bwlp.sat.database.mappers.DbLecture; +import org.openslx.bwlp.sat.database.mappers.DbLecturePermissions; import org.openslx.bwlp.sat.database.mappers.DbUser; import org.openslx.bwlp.sat.fileserv.ActiveDownload; import org.openslx.bwlp.sat.fileserv.ActiveUpload; @@ -28,6 +29,7 @@ import org.openslx.bwlp.thrift.iface.ImageDataError; import org.openslx.bwlp.thrift.iface.ImageDetailsRead; import org.openslx.bwlp.thrift.iface.ImagePermissions; import org.openslx.bwlp.thrift.iface.ImageSummaryRead; +import org.openslx.bwlp.thrift.iface.ImageVersionDetails; import org.openslx.bwlp.thrift.iface.ImageVersionWrite; import org.openslx.bwlp.thrift.iface.LecturePermissions; import org.openslx.bwlp.thrift.iface.LectureRead; @@ -158,8 +160,9 @@ public class ServerHandler implements SatelliteServer.Iface { @Override public void cancelDownload(String downloadToken) { - // TODO Auto-generated method stub - + ActiveDownload download = fileServer.getDownloadByToken(downloadToken); + if (download != null) + download.cancel(); } /* @@ -310,15 +313,6 @@ public class ServerHandler implements SatelliteServer.Iface { UserInfo user = SessionManager.getOrFail(userToken); User.canDeleteImageVersionOrFail(user, imageVersionId); try { - // Do not allow deleting remote images if share mode is set to "auto download" and - // the version to delete is the latest - ImageSummaryRead imageSummary = DbImage.getImageSummary(user, - DbImage.getBaseIdForVersionId(imageVersionId)); - if (imageSummary.shareMode == ShareMode.DOWNLOAD - && imageSummary.latestVersionId.equals(imageVersionId)) { - throw new TAuthorizationException(AuthorizationError.NO_PERMISSION, - "Cannot delete latest version of image if auto-download is enabled"); - } DbImage.markForDeletion(imageVersionId); } catch (SQLException e) { throw new TInternalServerError(); @@ -328,8 +322,21 @@ public class ServerHandler implements SatelliteServer.Iface { @Override public void deleteImageBase(String userToken, String imageBaseId) throws TAuthorizationException, TNotFoundException, TInternalServerError { - // TODO Auto-generated method stub - + UserInfo user = SessionManager.getOrFail(userToken); + ImageDetailsRead imageDetails; + try { + imageDetails = DbImage.getImageDetails(user, imageBaseId); + } catch (SQLException e) { + throw new TInternalServerError(); + } + User.canDeleteImageOrFail(imageDetails); + for (ImageVersionDetails version : imageDetails.versions) { + try { + DbImage.markForDeletion(version.versionId); + } catch (Exception e) { + LOGGER.warn("Could not delete version when trying to delete base image", e); + } + } } @Override @@ -449,22 +456,39 @@ public class ServerHandler implements SatelliteServer.Iface { @Override public void writeLecturePermissions(String userToken, String lectureId, - Map<String, LecturePermissions> permissions) throws TAuthorizationException, TNotFoundException { - // TODO Auto-generated method stub + Map<String, LecturePermissions> permissions) throws TAuthorizationException, TNotFoundException, + TInternalServerError { + UserInfo user = SessionManager.getOrFail(userToken); + User.canEditLecturePermissionsOrFail(user, lectureId); + try { + DbLecturePermissions.writeForLecture(lectureId, permissions); + } catch (SQLException e) { + throw new TInternalServerError(); + } } @Override public Map<String, LecturePermissions> getLecturePermissions(String userToken, String lectureId) - throws TAuthorizationException, TNotFoundException { - // TODO Auto-generated method stub - return null; + throws TAuthorizationException, TNotFoundException, TInternalServerError { + UserInfo user = SessionManager.getOrFail(userToken); + boolean adminOnly = !User.canEditLecturePermissions(user, lectureId); + try { + return DbLecturePermissions.getForLecture(lectureId, adminOnly); + } catch (SQLException e) { + throw new TInternalServerError(); + } } @Override public void setLectureOwner(String userToken, String lectureId, String newOwnerId) throws TAuthorizationException, TNotFoundException, TInternalServerError, TException { - // TODO Auto-generated method stub - + UserInfo user = SessionManager.getOrFail(userToken); + User.canChangeLectureOwnerOrFail(user, lectureId); + try { + DbLecture.setOwner(user, newOwnerId, lectureId); + } catch (SQLException e) { + throw new TInternalServerError(); + } } @Override diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/util/FileSystem.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/util/FileSystem.java index 38841cd9..c8f24e3f 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/util/FileSystem.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/util/FileSystem.java @@ -3,6 +3,8 @@ package org.openslx.bwlp.sat.util; import java.io.File; import org.apache.log4j.Logger; +import org.openslx.util.QuickTimer; +import org.openslx.util.QuickTimer.Task; public class FileSystem { @@ -22,4 +24,26 @@ public class FileSystem { return null; return file.substring(dir.length()); } + + /** + * Delete given file on the quicktimer thread, preventing the calling thread + * from freezing/hanging on I/O problems. + * + * @param file the file to delete + */ + public static void deleteAsync(final File file) { + QuickTimer.scheduleOnce(new Task() { + @Override + public void fire() { + if (!file.exists()) { + LOGGER.info("deleteAsync called for nonexistent file " + file.getAbsolutePath()); + return; + } + if (!file.delete()) { + LOGGER.warn("Could not delete file " + file.getAbsolutePath()); + } + } + }); + } + } |