|
package ir.openuniverse.tinify |
|
|
|
import java.io.File |
|
import java.nio.file.Files |
|
import java.nio.file.LinkOption.NOFOLLOW_LINKS |
|
import java.nio.file.Path |
|
import java.nio.file.StandardWatchEventKinds.* |
|
import java.nio.file.WatchEvent |
|
import java.nio.file.attribute.BasicFileAttributeView |
|
import java.nio.file.attribute.BasicFileAttributes |
|
|
|
/** |
|
* Created by [S. Mahdi Mir-Ismaili](https://mirismaili.github.io) on 1397/8/26 (17/11/2018). |
|
*/ |
|
open class WatchService(private val directoryPath: String) { |
|
private lateinit var deletionInformation: HashMap<String, DeletionInfo> |
|
protected lateinit var pendingEvents: MutableList<WatchEvent<*>> |
|
protected val files = HashMap<String, FileAttrs>() |
|
|
|
fun watch() { |
|
val folder = File(directoryPath) |
|
val path = folder.toPath() |
|
|
|
if (!folder.isDirectory) throw IllegalArgumentException("Path: '$path' is not a folder!") |
|
|
|
val fileSystem = path.fileSystem |
|
|
|
fileSystem.newWatchService().use { service: java.nio.file.WatchService -> |
|
path.register(service, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) |
|
|
|
logger.debug("Watching path: $path ...") |
|
|
|
for (file in folder.listFiles()) files[file.name] = getFileAttrs(file.toPath()) |
|
|
|
while (true) { // Start the infinite polling loop |
|
val key = service.take() // This command will be blocked until an event occurs ... |
|
|
|
deletionInformation = HashMap() |
|
pendingEvents = mutableListOf() |
|
|
|
for (watchEvent in key.pollEvents()) { |
|
//logger.debug("...................") |
|
pendingEvents.add(watchEvent) |
|
|
|
val kind = watchEvent.kind() |
|
|
|
val file = File("$path/${watchEvent.context()}") |
|
|
|
logger.debug("$kind: [${file.name}]") |
|
|
|
@Suppress("UNCHECKED_CAST") |
|
when (kind) { |
|
ENTRY_MODIFY -> { |
|
onEntryModifiedJ(file, watchEvent as WatchEvent<Path>) |
|
onEntryModified0(file, watchEvent) |
|
} |
|
ENTRY_CREATE -> { |
|
onEntryCreatedJ(file, watchEvent as WatchEvent<Path>) |
|
onEntryCreated0(file, watchEvent) |
|
} |
|
ENTRY_DELETE -> { |
|
onEntryDeletedJ(file, watchEvent as WatchEvent<Path>) |
|
onEntryDeleted0(file, watchEvent) |
|
} |
|
OVERFLOW -> onOverflowedJ(watchEvent) |
|
} |
|
} |
|
|
|
for (watchEvent in pendingEvents) { |
|
val kind = watchEvent.kind() |
|
|
|
val file = File("$path/${watchEvent.context()}") |
|
|
|
logger.debug("$kind: [${file.name}]") |
|
|
|
@Suppress("UNCHECKED_CAST") |
|
when (kind) { |
|
ENTRY_MODIFY -> onEntryModified(file, watchEvent as WatchEvent<Path>) |
|
ENTRY_CREATE -> onEntryCreated(file, watchEvent as WatchEvent<Path>) |
|
ENTRY_DELETE -> onEntryDeleted(file, watchEvent as WatchEvent<Path>) |
|
ENTRY_RENAME -> { |
|
watchEvent as RenameEvent |
|
onEntryRenamed(File("$path/${watchEvent.context0()}"), file, watchEvent) |
|
} |
|
OVERFLOW -> onOverflowed(watchEvent) |
|
} |
|
} |
|
|
|
if (!key.reset()) break |
|
|
|
logger.debug("------------------------") |
|
} |
|
} |
|
} |
|
|
|
protected open fun onEntryCreated0(newFile: File, watchEvent: WatchEvent<Path>) { |
|
logger.debug(newFile.name) |
|
val fileAttrs = getFileAttrs(newFile.toPath()) |
|
|
|
files[newFile.name] = fileAttrs |
|
|
|
var oldFileName: String? = null |
|
|
|
logger.trace(fileAttrs) |
|
|
|
for ((name, deletionInfo) in deletionInformation) |
|
if (deletionInfo.fileAttrs == fileAttrs) { |
|
oldFileName = name |
|
break |
|
} |
|
|
|
logger.debug("Old file name: $oldFileName") |
|
if (oldFileName == null) return |
|
|
|
val deleteEvent = deletionInformation[oldFileName]!!.watchEvent |
|
//val delay = System.currentTimeMillis() - deletionInfo.deletionTime |
|
|
|
// 1. Remove DeleteEvent: |
|
pendingEvents.remove(deleteEvent) |
|
|
|
// 2. Replace CreateEvent with RenameEvent: |
|
pendingEvents[pendingEvents.indexOf(watchEvent)] = RenameEvent(deleteEvent.context() as Path, watchEvent.context() as Path) |
|
|
|
deletionInformation.remove(oldFileName) |
|
} |
|
|
|
protected open fun onEntryModified0(file: File, watchEvent: WatchEvent<Path>) { |
|
logger.debug(file.name) |
|
files[file.name] = getFileAttrs(file.toPath()) |
|
} |
|
|
|
protected open fun onEntryDeleted0(oldFile: File, watchEvent: WatchEvent<Path>) { |
|
logger.debug(oldFile.name) |
|
deletionInformation[oldFile.name] = DeletionInfo(files.remove(oldFile.name)!!, watchEvent, System.currentTimeMillis()) |
|
} |
|
|
|
protected fun getFileAttrs(path: Path) = FileAttrs(Files.getFileAttributeView( |
|
path, BasicFileAttributeView::class.java, NOFOLLOW_LINKS).readAttributes()) |
|
|
|
data class DeletionInfo(val fileAttrs: FileAttrs, val watchEvent: WatchEvent<*>, val deletionTime: Long) |
|
|
|
companion object { |
|
@Suppress("unused", "MemberVisibilityCanBePrivate") |
|
class FileAttrs(attributes: BasicFileAttributes) { |
|
val size = attributes.size() |
|
val creationTime = attributes.creationTime().toMillis() |
|
val lastModifiedTime = attributes.lastModifiedTime().toMillis() |
|
val lastAccessTime = attributes.lastAccessTime().toMillis() |
|
val isRegularFile = attributes.isRegularFile |
|
val isDirectory = attributes.isDirectory |
|
val isSymbolicLink = attributes.isSymbolicLink |
|
val isOther = attributes.isOther |
|
|
|
override fun equals(other: Any?): Boolean = areEqual(this, other) |
|
|
|
override fun toString(): String = toJson(this) |
|
|
|
override fun hashCode(): Int { |
|
var result = size.hashCode() |
|
result = 31 * result + creationTime.hashCode() |
|
result = 31 * result + lastModifiedTime.hashCode() |
|
result = 31 * result + lastAccessTime.hashCode() |
|
result = 31 * result + isRegularFile.hashCode() |
|
result = 31 * result + isDirectory.hashCode() |
|
result = 31 * result + isSymbolicLink.hashCode() |
|
result = 31 * result + isOther.hashCode() |
|
return result |
|
} |
|
} |
|
|
|
val ENTRY_RENAME = object : WatchEvent.Kind<Path> { |
|
private val name = "ENTRY_RENAME" |
|
|
|
override fun name(): String = name |
|
override fun type(): Class<Path> = Path::class.java |
|
override fun toString(): String = name |
|
} |
|
|
|
class RenameEvent(private val context0: Path, private val context1: Path) : Event(ENTRY_RENAME, context1) { |
|
fun context0(): Path = context0 |
|
fun context1(): Path = context1 |
|
} |
|
|
|
abstract class Event(private val kind: WatchEvent.Kind<Path>, private val context: Path, private var count: Int = 1) : WatchEvent<Path> { |
|
override fun count(): Int = count |
|
override fun kind(): WatchEvent.Kind<Path> = kind |
|
override fun context(): Path = context |
|
internal fun increment() = ++count |
|
} |
|
|
|
class CreateEvent(context: Path) : Event(ENTRY_CREATE, context) |
|
class DeleteEvent(context: Path) : Event(ENTRY_DELETE, context) |
|
class ModifyEvent(context: Path) : Event(ENTRY_MODIFY, context) |
|
} |
|
|
|
protected open fun onEntryCreated(newFile: File, watchEvent: WatchEvent<Path>) {} |
|
protected open fun onEntryRenamed(oldFile: File, newFile: File, renameEvent: RenameEvent) {} |
|
protected open fun onEntryModified(file: File, watchEvent: WatchEvent<Path>) {} |
|
protected open fun onEntryDeleted(oldFile: File, watchEvent: WatchEvent<Path>) {} |
|
protected open fun onOverflowed(watchEvent: WatchEvent<*>) {} |
|
|
|
protected open fun onEntryCreatedJ(newFile: File, watchEvent: WatchEvent<Path>) = logger.trace(newFile.name) |
|
protected open fun onEntryModifiedJ(file: File, watchEvent: WatchEvent<Path>) = logger.trace(file.name) |
|
protected open fun onEntryDeletedJ(oldFile: File, watchEvent: WatchEvent<Path>) = logger.trace(oldFile.name) |
|
protected open fun onOverflowedJ(watchEvent: WatchEvent<*>) {} |
|
} |