Skip to content

Commit

Permalink
Improve module updates by backing up files
Browse files Browse the repository at this point in the history
  • Loading branch information
jaakkonakaza committed Aug 12, 2024
1 parent 098479e commit cc9f188
Show file tree
Hide file tree
Showing 16 changed files with 652 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ abstract class Component<T>(val name: String, protected val project: Project) {

abstract fun load()

abstract suspend fun downloadAndInstall()
abstract suspend fun downloadAndInstall(updating: Boolean = false)

abstract suspend fun remove(deleteFiles: Boolean)
}
85 changes: 70 additions & 15 deletions src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Module.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fi.aalto.cs.apluscourses.model.component

import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
Expand All @@ -9,12 +11,23 @@ import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.LibraryOrderEntry
import com.intellij.openapi.roots.ModuleOrderEntry
import com.intellij.openapi.roots.RootPolicy
import com.intellij.util.io.createParentDirectories
import fi.aalto.cs.apluscourses.notifications.ModuleUpdatedNotification
import fi.aalto.cs.apluscourses.services.Notifier
import fi.aalto.cs.apluscourses.services.PluginSettings
import fi.aalto.cs.apluscourses.services.course.CourseFileManager
import fi.aalto.cs.apluscourses.services.course.CourseFileManager.ModuleMetadata
import fi.aalto.cs.apluscourses.services.course.CourseManager
import fi.aalto.cs.apluscourses.ui.module.UpdateModuleDialog
import fi.aalto.cs.apluscourses.utils.FileUtil
import fi.aalto.cs.apluscourses.utils.Version
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import kotlin.io.path.exists
import kotlin.io.path.moveTo
import com.intellij.openapi.module.Module as IdeaModule

class Module(
Expand Down Expand Up @@ -58,17 +71,27 @@ class Module(
val documentationExists: Boolean
get() = status == Status.LOADED && documentationIndexFullPath.toFile().exists()

// fun hasLocalChanges(downloadedAt: ZonedDateTime): Boolean {
// val fullPath = fullPath
// val timeStamp = (downloadedAt.toInstant().toEpochMilli()
// + PluginSettings.REASONABLE_DELAY_FOR_MODULE_INSTALLATION)
// return false
//// return ReadAction.compute<Boolean, RuntimeException> { VfsUtil.hasDirectoryChanges(fullPath, timeStamp) }
// }
fun changedFiles(): List<Path> {
val fullPath = fullPath
val timestamp = metadata?.downloadedAt ?: return emptyList()
val timeStamp = timestamp.toEpochMilliseconds() + PluginSettings.REASONABLE_DELAY_FOR_MODULE_INSTALLATION
return ReadAction.compute<List<Path>, RuntimeException> {
FileUtil.getChangedFilesInDirectory(
fullPath.toFile(),
timeStamp
)
}
}

override suspend fun downloadAndInstall() {
if (platformObject != null) {
return
override suspend fun downloadAndInstall(updating: Boolean) {
val oldPlatformObject = platformObject
if (oldPlatformObject != null) {
if (!updating) {
return
}
writeAction {
ModuleManager.getInstance(project).disposeModule(oldPlatformObject)
}
}
status = Status.LOADING
downloadAndUnzipZip(zipUrl, Path.of(project.basePath!!))
Expand All @@ -86,6 +109,39 @@ class Module(
}
}

suspend fun update() {
val filesWithChanges = changedFiles()
val allFiles = FileUtil.getAllFilesInDirectory(fullPath.toFile())
if (filesWithChanges.isNotEmpty()) {
val canceled = withContext(Dispatchers.EDT) {
!UpdateModuleDialog(project, this@Module, filesWithChanges).showAndGet()
}
if (canceled) return
val backupDir = fullPath.resolve("backup")

for (file in filesWithChanges) {
val relativePath = fullPath.relativize(file)
val targetPath = backupDir.resolve(relativePath)

if (!targetPath.parent.exists()) {
targetPath.createParentDirectories()
}
file.moveTo(targetPath, StandardCopyOption.REPLACE_EXISTING)
}
}

FileUtil.deleteFilesInDirectory(fullPath.toFile(), fullPath.resolve("backup"))
downloadAndInstall(updating = true)

val newFiles = FileUtil.getAllFilesInDirectory(fullPath.toFile())
val deletedFiles = allFiles - newFiles
val addedFiles = newFiles - allFiles
Notifier.notifyAndHide(
ModuleUpdatedNotification(this, addedFiles, deletedFiles),
project
)
}

private val imlPath
get() = fullPath.resolve("$name.iml")

Expand Down Expand Up @@ -131,7 +187,7 @@ class Module(
}

val isUpdateAvailable: Boolean
get() = metadata != null && latestVersion > metadata!!.version
get() = status == Status.LOADED && metadata != null && latestVersion > metadata!!.version

val isMinorUpdate: Boolean
get() = isUpdateAvailable && latestVersion.major == metadata!!.version.major
Expand All @@ -142,11 +198,10 @@ class Module(

val category: Category
get() {
return if (status == Status.LOADED) {
Category.INSTALLED
// } else if (status == Status.ERROR || dependencyState == OldComponent.DEP_ERROR || isUpdateAvailable) {
} else if (status == Status.ERROR || isUpdateAvailable) {
return if (status == Status.ERROR || isUpdateAvailable) {
Category.ACTION_REQUIRED
} else if (status == Status.LOADED) {
Category.INSTALLED
} else {
Category.AVAILABLE
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
package fi.aalto.cs.apluscourses.model.component

import com.intellij.openapi.application.readAndWriteAction
import com.intellij.openapi.roots.libraries.Library as IdeaLibrary
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProviderImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.impl.libraries.LibraryEx
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar
import com.intellij.openapi.util.io.systemIndependentPath
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.util.application
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.jetbrains.plugins.scala.project.ScalaLibraryType
import org.jetbrains.plugins.scala.project.external.ScalaSdkUtils
import scala.Option
import scala.jdk.javaapi.CollectionConverters
import java.io.File
import java.nio.file.Path
import kotlin.io.path.pathString

class ScalaSdk(private val scalaVersion: String, project: Project) : Library(scalaVersion, project) {
override suspend fun downloadAndInstall() {
override suspend fun downloadAndInstall(updating: Boolean) {
println("Downloading Scala SDK $scalaVersion")
val strippedScalaVersion = this.scalaVersion.substringAfter("scala-sdk-")
val zipUrl =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package fi.aalto.cs.apluscourses.notifications

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.util.io.isFile
import fi.aalto.cs.apluscourses.model.component.Module
import fi.aalto.cs.apluscourses.services.PluginSettings
import java.nio.file.Path
import kotlin.io.path.isRegularFile

class ModuleUpdatedNotification(module: Module, addedFiles: List<Path>, removedFiles: List<Path>) : Notification(
PluginSettings.A_PLUS,
"Module Updated",
notificationContent(module, addedFiles, removedFiles),
NotificationType.INFORMATION
) {
companion object {
private fun pathsToHtmlList(paths: List<Path>, module: Module): String {
fun relativePath(path: Path): String {
return module.fullPath.relativize(path).toString()
}
return paths.filter {
it.toString().contains('.')
} // Filter out directories, while keeping removed files
.joinToString(separator = "") { "<li>${relativePath(it)}</li>" }
}

private fun notificationContent(module: Module, addedFiles: List<Path>, removedFiles: List<Path>): String {
val baseText = "${module.name} has been updated to version ${module.latestVersion}.<vr>"
val addedFilesText =
if (addedFiles.isNotEmpty()) "Added files: <ul>${pathsToHtmlList(addedFiles, module)}</ul>" else null
val removedFilesText =
if (removedFiles.isNotEmpty())
"<br>Removed files: <ul>${pathsToHtmlList(removedFiles, module)}</ul>" else null
return listOfNotNull(baseText, addedFilesText, removedFilesText).joinToString(separator = "<br>")
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/fi/aalto/cs/apluscourses/services/Background.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package fi.aalto.cs.apluscourses.services

import com.intellij.openapi.components.Service
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@Service(Service.Level.PROJECT)
class Background(
val cs: CoroutineScope
) {
fun runInBackground(action: suspend () -> Unit) {
cs.launch {
action()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ class CoursesClient(
val client: HttpClient by lazy {
HttpClient(CIO) {
install(Resources)
install(HttpCache) {
val cacheFile =
Files.createDirectories(Path(project.basePath!!).resolve(".idea/aplusCourses/")).toFile()
privateStorage(FileStorage(cacheFile))
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class Notifier(
* @param project The Project where the Notification is shown.
* @param timeoutMs The time in milliseconds after which the notification is hidden.
*/
fun notifyAndHide(notification: Notification, project: Project, timeoutMs: Long = 6000L) {
fun notifyAndHide(notification: Notification, project: Project, timeoutMs: Long = 10000L) {
getInstance(project).notifyAndHide(notification, timeoutMs)
}
}
Expand Down
37 changes: 36 additions & 1 deletion src/main/kotlin/fi/aalto/cs/apluscourses/services/Opener.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package fi.aalto.cs.apluscourses.services

import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.ide.browsers.actions.OpenInBrowserBaseGroupAction.OpenInBrowserEditorContextBarGroupAction
import com.intellij.ide.projectView.ProjectView
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
Expand Down Expand Up @@ -45,6 +47,38 @@ class Opener(
}
}

fun openDocumentation(module: Module) {
cs.launch {
val dir = module.platformObject?.guessModuleDir() ?: return@launch
val virtualFile = dir.findFile("doc/index.html") ?: return@launch
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return@launch
val newDataContext = SimpleDataContext.builder().add(CommonDataKeys.PROJECT, project)
.add(CommonDataKeys.PSI_FILE, psiFile).build()
val actionEvent = AnActionEvent.createFromDataContext(ActionPlaces.TOOLWINDOW_CONTENT, null, newDataContext)
val action = OpenInBrowserEditorContextBarGroupAction().getChildren(null)[0]
withContext(Dispatchers.EDT) {
action.actionPerformed(actionEvent)
}
}
}

fun showModuleInProjectTreeAction(module: Module): AnAction {
return object : DumbAwareAction("Show in Project Tree") {
override fun actionPerformed(e: AnActionEvent) {
showModuleInProjectTree(module)
}

override fun update(e: AnActionEvent) {
e.presentation.text = "Show in Project Tree"
e.presentation.icon = AllIcons.General.Locate
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
}
}

fun openDocumentationAction(module: Module, fileName: String): AnAction {
return object : DumbAwareAction("Show Documentation") {
override fun actionPerformed(e: AnActionEvent) {
Expand All @@ -64,11 +98,12 @@ class Opener(
}

override fun update(e: AnActionEvent) {
e.presentation.text = "Show Documentation"
e.presentation.icon = PluginIcons.A_PLUS_DOCS
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
return ActionUpdateThread.EDT
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ class CourseManager(
}
}

fun updateModule(module: Module) {
cs.launch {
module.update()
refreshModuleStatuses()
}
}

private fun getMissingDependencies(module: Module): List<Component<*>> {
return module.dependencyNames
?.mapNotNull { state.course?.getComponentIfExists(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class SubmitExercise(


val canceled = withContext(Dispatchers.EDT) {
return@withContext !SubmitExerciseDialog(project, exercise, files.values.toList()).showAndGet()
!SubmitExerciseDialog(project, exercise, files.values.toList()).showAndGet()
}

if (canceled) {
Expand Down
Loading

0 comments on commit cc9f188

Please sign in to comment.