mirror of
https://github.com/android-password-store/Android-Password-Store.git
synced 2026-05-02 06:10:04 +02:00
refactor: migrate to NIO
This commit is contained in:
parent
7010ee85b4
commit
36b45f90f9
|
|
@ -9,19 +9,25 @@ import android.content.Intent
|
|||
import app.passwordstore.data.repo.PasswordRepository
|
||||
import app.passwordstore.ui.crypto.BasePGPActivity
|
||||
import app.passwordstore.ui.main.LaunchActivity
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.nameWithoutExtension
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.relativeTo
|
||||
|
||||
data class PasswordItem(
|
||||
val name: String,
|
||||
val parent: PasswordItem? = null,
|
||||
val type: Char,
|
||||
val file: File,
|
||||
val rootDir: File,
|
||||
val file: Path,
|
||||
val rootDir: Path,
|
||||
) : Comparable<PasswordItem> {
|
||||
|
||||
val fullPathToParent = file.absolutePath.replace(rootDir.absolutePath, "").replace(file.name, "")
|
||||
val name = file.nameWithoutExtension
|
||||
|
||||
val longName = BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePath, toString())
|
||||
val fullPathToParent = file.relativeTo(rootDir).parent.pathString
|
||||
|
||||
val longName =
|
||||
BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePathString(), toString())
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other is PasswordItem) && (other.file == file)
|
||||
|
|
@ -32,7 +38,7 @@ data class PasswordItem(
|
|||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return name.replace("\\.gpg$".toRegex(), "")
|
||||
return name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
|
@ -43,8 +49,8 @@ data class PasswordItem(
|
|||
fun createAuthEnabledIntent(context: Context): Intent {
|
||||
val intent = Intent(context, LaunchActivity::class.java)
|
||||
intent.putExtra("NAME", toString())
|
||||
intent.putExtra("FILE_PATH", file.absolutePath)
|
||||
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath)
|
||||
intent.putExtra("FILE_PATH", file.absolutePathString())
|
||||
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePathString())
|
||||
intent.action = LaunchActivity.ACTION_DECRYPT_PASS
|
||||
return intent
|
||||
}
|
||||
|
|
@ -55,23 +61,23 @@ data class PasswordItem(
|
|||
const val TYPE_PASSWORD = 'p'
|
||||
|
||||
@JvmStatic
|
||||
fun newCategory(name: String, file: File, parent: PasswordItem, rootDir: File): PasswordItem {
|
||||
return PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir)
|
||||
fun newCategory(path: Path, parent: PasswordItem, rootDir: Path): PasswordItem {
|
||||
return PasswordItem(parent, TYPE_CATEGORY, path, rootDir)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newCategory(name: String, file: File, rootDir: File): PasswordItem {
|
||||
return PasswordItem(name, null, TYPE_CATEGORY, file, rootDir)
|
||||
fun newCategory(path: Path, rootDir: Path): PasswordItem {
|
||||
return PasswordItem(null, TYPE_CATEGORY, path, rootDir)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newPassword(name: String, file: File, parent: PasswordItem, rootDir: File): PasswordItem {
|
||||
return PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir)
|
||||
fun newPassword(path: Path, parent: PasswordItem, rootDir: Path): PasswordItem {
|
||||
return PasswordItem(parent, TYPE_PASSWORD, path, rootDir)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newPassword(name: String, file: File, rootDir: File): PasswordItem {
|
||||
return PasswordItem(name, null, TYPE_PASSWORD, file, rootDir)
|
||||
fun newPassword(path: Path, rootDir: Path): PasswordItem {
|
||||
return PasswordItem(null, TYPE_PASSWORD, path, rootDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ package app.passwordstore.data.repo
|
|||
|
||||
import androidx.core.content.edit
|
||||
import app.passwordstore.Application
|
||||
import app.passwordstore.data.password.PasswordItem
|
||||
import app.passwordstore.util.extensions.sharedPrefs
|
||||
import app.passwordstore.util.extensions.unsafeLazy
|
||||
import app.passwordstore.util.settings.PasswordSortOrder
|
||||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.PathWalkOption
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.walk
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
import org.eclipse.jgit.lib.Repository
|
||||
|
|
@ -23,12 +27,13 @@ import org.eclipse.jgit.transport.RefSpec
|
|||
import org.eclipse.jgit.transport.RemoteConfig
|
||||
import org.eclipse.jgit.transport.URIish
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
object PasswordRepository {
|
||||
|
||||
var repository: Repository? = null
|
||||
private val settings by unsafeLazy { Application.instance.sharedPrefs }
|
||||
private val filesDir
|
||||
get() = Application.instance.filesDir
|
||||
get() = Application.instance.filesDir.toPath()
|
||||
|
||||
val isInitialized: Boolean
|
||||
get() = repository != null
|
||||
|
|
@ -41,19 +46,20 @@ object PasswordRepository {
|
|||
* Takes in a [repositoryDir] to initialize a Git repository with, and assigns it to [repository]
|
||||
* as static state.
|
||||
*/
|
||||
private fun initializeRepository(repositoryDir: File) {
|
||||
private fun initializeRepository(repositoryDir: Path) {
|
||||
val builder = FileRepositoryBuilder()
|
||||
repository =
|
||||
runCatching { builder.setGitDir(repositoryDir).build() }
|
||||
runCatching { builder.setGitDir(repositoryDir.toFile()).build() }
|
||||
.getOrElse { e ->
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun createRepository(repositoryDir: File) {
|
||||
repositoryDir.delete()
|
||||
repository = Git.init().setDirectory(repositoryDir).call().repository
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
fun createRepository(repositoryDir: Path) {
|
||||
repositoryDir.deleteRecursively()
|
||||
repository = Git.init().setDirectory(repositoryDir.toFile()).call().repository
|
||||
}
|
||||
|
||||
// TODO add multiple remotes support for pull/push
|
||||
|
|
@ -106,8 +112,8 @@ object PasswordRepository {
|
|||
repository = null
|
||||
}
|
||||
|
||||
fun getRepositoryDirectory(): File {
|
||||
return File(filesDir.toString(), "/store")
|
||||
fun getRepositoryDirectory(): Path {
|
||||
return filesDir.resolve("store")
|
||||
}
|
||||
|
||||
fun initialize(): Repository? {
|
||||
|
|
@ -116,8 +122,8 @@ object PasswordRepository {
|
|||
settings.edit {
|
||||
if (
|
||||
!dir.exists() ||
|
||||
!dir.isDirectory ||
|
||||
requireNotNull(dir.listFiles()) { "Failed to list files in ${dir.path}" }.isEmpty()
|
||||
!dir.isDirectory() ||
|
||||
dir.walk(PathWalkOption.INCLUDE_DIRECTORIES).toList().isEmpty()
|
||||
) {
|
||||
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
|
||||
} else {
|
||||
|
|
@ -141,53 +147,4 @@ object PasswordRepository {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the .gpg files in a directory
|
||||
*
|
||||
* @param path the directory path
|
||||
* @return the list of gpg files in that directory
|
||||
*/
|
||||
private fun getFilesList(path: File): ArrayList<File> {
|
||||
if (!path.exists()) return ArrayList()
|
||||
val files =
|
||||
(path.listFiles { file -> file.isDirectory || file.extension == "gpg" } ?: emptyArray())
|
||||
.toList()
|
||||
val items = ArrayList<File>()
|
||||
items.addAll(files)
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the passwords (PasswordItem) in a directory
|
||||
*
|
||||
* @param path the directory path
|
||||
* @return a list of password items
|
||||
*/
|
||||
fun getPasswords(
|
||||
path: File,
|
||||
rootDir: File,
|
||||
sortOrder: PasswordSortOrder,
|
||||
): ArrayList<PasswordItem> {
|
||||
// We need to recover the passwords then parse the files
|
||||
val passList = getFilesList(path).also { it.sortBy { f -> f.name } }
|
||||
val passwordList = ArrayList<PasswordItem>()
|
||||
val showHidden = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
|
||||
|
||||
if (passList.size == 0) return passwordList
|
||||
if (!showHidden) {
|
||||
passList.filter { !it.isHidden }.toCollection(passList.apply { clear() })
|
||||
}
|
||||
passList.forEach { file ->
|
||||
passwordList.add(
|
||||
if (file.isFile) {
|
||||
PasswordItem.newPassword(file.name, file, rootDir)
|
||||
} else {
|
||||
PasswordItem.newCategory(file.name, file, rootDir)
|
||||
}
|
||||
)
|
||||
}
|
||||
passwordList.sortWith(sortOrder.comparator)
|
||||
return passwordList
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import app.passwordstore.data.password.PasswordItem
|
|||
import app.passwordstore.util.coroutines.DispatcherProvider
|
||||
import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter
|
||||
import app.passwordstore.util.viewmodel.stableId
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
|
@ -71,7 +74,10 @@ open class PasswordItemRecyclerAdapter(
|
|||
folderIndicator.visibility = View.VISIBLE
|
||||
val count =
|
||||
withContext(dispatcherProvider.io()) {
|
||||
item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0
|
||||
item.file
|
||||
.listDirectoryEntries()
|
||||
.filter { it.isDirectory() || it.extension == "gpg" }
|
||||
.size
|
||||
}
|
||||
childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
|
||||
childCount.text = "$count"
|
||||
|
|
|
|||
|
|
@ -39,8 +39,11 @@ import com.github.michaelbull.result.onSuccess
|
|||
import com.github.michaelbull.result.runCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.readBytes
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority.ERROR
|
||||
|
|
@ -59,12 +62,14 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
val filePath =
|
||||
intent?.getStringExtra(EXTRA_FILE_PATH)
|
||||
?: run {
|
||||
logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
|
||||
finish()
|
||||
return
|
||||
}
|
||||
Paths.get(
|
||||
intent?.getStringExtra(EXTRA_FILE_PATH)
|
||||
?: run {
|
||||
logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
|
||||
finish()
|
||||
return
|
||||
}
|
||||
)
|
||||
val clientState =
|
||||
intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE)
|
||||
?: run {
|
||||
|
|
@ -93,14 +98,17 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
}
|
||||
|
||||
private fun decrypt(
|
||||
filePath: String,
|
||||
filePath: Path,
|
||||
clientState: Bundle,
|
||||
action: AutofillAction,
|
||||
authResult: BiometricResult,
|
||||
) {
|
||||
val gpgIdentifiers =
|
||||
getPGPIdentifiers(
|
||||
getParentPath(filePath, PasswordRepository.getRepositoryDirectory().toString())
|
||||
getParentPath(
|
||||
filePath.absolutePathString(),
|
||||
PasswordRepository.getRepositoryDirectory().toString(),
|
||||
)
|
||||
) ?: return
|
||||
lifecycleScope.launch(dispatcherProvider.main()) {
|
||||
when (authResult) {
|
||||
|
|
@ -126,13 +134,7 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
gpgIdentifiers.first(),
|
||||
)
|
||||
if (cachedPassphrase != null) {
|
||||
decryptWithPassphrase(
|
||||
File(filePath),
|
||||
gpgIdentifiers,
|
||||
clientState,
|
||||
action,
|
||||
cachedPassphrase,
|
||||
)
|
||||
decryptWithPassphrase(filePath, gpgIdentifiers, clientState, action, cachedPassphrase)
|
||||
} else {
|
||||
askPassphrase(filePath, gpgIdentifiers, clientState, action)
|
||||
}
|
||||
|
|
@ -142,13 +144,13 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
}
|
||||
|
||||
private suspend fun askPassphrase(
|
||||
filePath: String,
|
||||
filePath: Path,
|
||||
identifiers: List<PGPIdentifier>,
|
||||
clientState: Bundle,
|
||||
action: AutofillAction,
|
||||
) {
|
||||
if (!repository.isPasswordProtected(identifiers)) {
|
||||
decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "")
|
||||
decryptWithPassphrase(filePath, identifiers, clientState, action, password = "")
|
||||
return
|
||||
}
|
||||
val dialog =
|
||||
|
|
@ -162,14 +164,14 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
val value = bundle.getString(PasswordDialog.PASSWORD_PHRASE_KEY)!!
|
||||
clearCache = bundle.getBoolean(PasswordDialog.PASSWORD_CLEAR_KEY)
|
||||
lifecycleScope.launch(dispatcherProvider.main()) {
|
||||
decryptWithPassphrase(File(filePath), identifiers, clientState, action, value)
|
||||
decryptWithPassphrase(filePath, identifiers, clientState, action, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decryptWithPassphrase(
|
||||
filePath: File,
|
||||
filePath: Path,
|
||||
identifiers: List<PGPIdentifier>,
|
||||
clientState: Bundle,
|
||||
action: AutofillAction,
|
||||
|
|
@ -199,7 +201,7 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
}
|
||||
|
||||
private suspend fun decryptCredential(
|
||||
file: File,
|
||||
file: Path,
|
||||
password: String,
|
||||
identifiers: List<PGPIdentifier>,
|
||||
): Credentials? {
|
||||
|
|
@ -240,19 +242,19 @@ class AutofillDecryptActivity : BasePGPActivity() {
|
|||
|
||||
private var decryptFileRequestCode = 1
|
||||
|
||||
fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent {
|
||||
fun makeDecryptFileIntent(file: Path, forwardedExtras: Bundle, context: Context): Intent {
|
||||
return Intent(context, AutofillDecryptActivity::class.java).apply {
|
||||
putExtras(forwardedExtras)
|
||||
putExtra(EXTRA_SEARCH_ACTION, true)
|
||||
putExtra(EXTRA_FILE_PATH, file.absolutePath)
|
||||
putExtra(EXTRA_FILE_PATH, file.absolutePathString())
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender {
|
||||
fun makeDecryptFileIntentSender(file: Path, context: Context): IntentSender {
|
||||
val intent =
|
||||
Intent(context, AutofillDecryptActivity::class.java).apply {
|
||||
putExtra(EXTRA_SEARCH_ACTION, false)
|
||||
putExtra(EXTRA_FILE_PATH, file.absolutePath)
|
||||
putExtra(EXTRA_FILE_PATH, file.absolutePathString())
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
|
|||
import com.github.androidpasswordstore.autofillparser.FormOrigin
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlin.io.path.relativeTo
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.logcat
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ import com.github.androidpasswordstore.autofillparser.AutofillAction
|
|||
import com.github.androidpasswordstore.autofillparser.Credentials
|
||||
import com.github.androidpasswordstore.autofillparser.FormOrigin
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.absolutePathString
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.logcat
|
||||
|
||||
|
|
@ -109,8 +110,9 @@ class AutofillSaveActivity : AppCompatActivity() {
|
|||
Intent(this, PasswordCreationActivity::class.java).apply {
|
||||
putExtras(
|
||||
bundleOf(
|
||||
"REPO_PATH" to repo.absolutePath,
|
||||
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
|
||||
"REPO_PATH" to repo.absolutePathString(),
|
||||
"FILE_PATH" to
|
||||
repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePathString(),
|
||||
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
|
||||
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
|
||||
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to
|
||||
|
|
@ -122,7 +124,7 @@ class AutofillSaveActivity : AppCompatActivity() {
|
|||
val data = result.data
|
||||
if (result.resultCode == RESULT_OK && data != null) {
|
||||
val createdPath = data.getStringExtra("CREATED_FILE")!!
|
||||
formOrigin?.let { AutofillMatcher.addMatchFor(this, it, File(createdPath)) }
|
||||
formOrigin?.let { AutofillMatcher.addMatchFor(this, it, Paths.get(createdPath)) }
|
||||
val password = data.getStringExtra("PASSWORD")
|
||||
val resultIntent =
|
||||
if (password != null) {
|
||||
|
|
|
|||
|
|
@ -34,8 +34,15 @@ import app.passwordstore.util.settings.PreferenceKeys
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.nameWithoutExtension
|
||||
import kotlin.io.path.readLines
|
||||
import kotlin.io.path.readText
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
|
@ -54,7 +61,7 @@ open class BasePGPActivity : AppCompatActivity() {
|
|||
*
|
||||
* Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org
|
||||
*/
|
||||
val name: String by unsafeLazy { File(fullPath).nameWithoutExtension }
|
||||
val name: String by unsafeLazy { Paths.get(fullPath).nameWithoutExtension }
|
||||
|
||||
/** Action to invoke if [keyImportAction] succeeds. */
|
||||
private var onKeyImport: (() -> Unit)? = null
|
||||
|
|
@ -155,8 +162,8 @@ open class BasePGPActivity : AppCompatActivity() {
|
|||
fun getPGPIdentifiers(subDir: String): List<PGPIdentifier>? {
|
||||
val repoRoot = PasswordRepository.getRepositoryDirectory()
|
||||
val gpgIdentifierFile =
|
||||
File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot)
|
||||
?: File(repoRoot, ".gpg-id").apply { createNewFile() }
|
||||
repoRoot.resolve(subDir).findTillRoot(".gpg-id", repoRoot)
|
||||
?: repoRoot.resolve(".gpg-id").createFile()
|
||||
val gpgIdentifiers =
|
||||
gpgIdentifierFile
|
||||
.readLines()
|
||||
|
|
@ -185,15 +192,13 @@ open class BasePGPActivity : AppCompatActivity() {
|
|||
return gpgIdentifiers
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun File.findTillRoot(fileName: String, rootPath: File): File? {
|
||||
val gpgFile = File(this, fileName)
|
||||
private fun Path.findTillRoot(fileName: String, rootPath: Path): Path? {
|
||||
val gpgFile = this.resolve(fileName)
|
||||
if (gpgFile.exists()) return gpgFile
|
||||
|
||||
if (this.absolutePath == rootPath.absolutePath) {
|
||||
if (this.absolutePathString() == rootPath.absolutePathString()) {
|
||||
return null
|
||||
}
|
||||
val parent = parentFile
|
||||
return if (parent != null && parent.exists()) {
|
||||
parent.findTillRoot(fileName, rootPath)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ import app.passwordstore.util.settings.Constants
|
|||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.readBytes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -233,7 +234,8 @@ class DecryptActivity : BasePGPActivity() {
|
|||
authResult: BiometricResult,
|
||||
onSuccess: suspend () -> Unit = {},
|
||||
) {
|
||||
val message = withContext(dispatcherProvider.io()) { File(fullPath).readBytes().inputStream() }
|
||||
val message =
|
||||
withContext(dispatcherProvider.io()) { Paths.get(fullPath).readBytes().inputStream() }
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val result = repository.decrypt(passphrase, identifiers, message, outputStream)
|
||||
if (result.isOk) {
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ class PasswordCreationActivity : BasePGPActivity() {
|
|||
return@runCatching
|
||||
}
|
||||
|
||||
if (!passwordFile.toFile().isInsideRepository()) {
|
||||
if (!passwordFile.isInsideRepository()) {
|
||||
snackbar(message = getString(R.string.message_error_destination_outside_repo))
|
||||
return@runCatching
|
||||
}
|
||||
|
|
@ -414,8 +414,7 @@ class PasswordCreationActivity : BasePGPActivity() {
|
|||
val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
|
||||
val entry = passwordEntryFactory.create(content.encodeToByteArray())
|
||||
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
|
||||
val username =
|
||||
entry.username ?: directoryStructure.getUsernameFor(passwordFile.toFile())
|
||||
val username = entry.username ?: directoryStructure.getUsernameFor(passwordFile)
|
||||
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,15 @@ import app.passwordstore.ui.passwords.PasswordStore
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.isRegularFile
|
||||
|
||||
class FolderCreationDialogFragment : DialogFragment() {
|
||||
|
||||
private lateinit var newFolder: File
|
||||
private lateinit var newFolder: Path
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
|
||||
|
|
@ -41,15 +45,15 @@ class FolderCreationDialogFragment : DialogFragment() {
|
|||
val dialog = requireDialog()
|
||||
val folderNameView = dialog.findViewById<TextInputEditText>(R.id.folder_name_text)
|
||||
val folderNameViewContainer = dialog.findViewById<TextInputLayout>(R.id.folder_name_container)
|
||||
newFolder = File("$currentDir/${folderNameView.text}")
|
||||
newFolder = Paths.get("$currentDir/${folderNameView.text}")
|
||||
folderNameViewContainer.error =
|
||||
when {
|
||||
newFolder.isFile -> getString(R.string.folder_creation_err_file_exists)
|
||||
newFolder.isDirectory -> getString(R.string.folder_creation_err_folder_exists)
|
||||
newFolder.isRegularFile() -> getString(R.string.folder_creation_err_file_exists)
|
||||
newFolder.isDirectory() -> getString(R.string.folder_creation_err_folder_exists)
|
||||
else -> null
|
||||
}
|
||||
if (folderNameViewContainer.error != null) return
|
||||
newFolder.mkdirs()
|
||||
newFolder.createDirectories()
|
||||
(requireActivity() as PasswordStore).refreshPasswordList(newFolder)
|
||||
// TODO(msfjarvis): Restore this functionality
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import app.passwordstore.data.repo.PasswordRepository
|
|||
import app.passwordstore.ui.passwords.PASSWORD_FRAGMENT_TAG
|
||||
import app.passwordstore.ui.passwords.PasswordStore
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
|
||||
|
|
@ -28,7 +29,7 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
|
|||
val args = Bundle()
|
||||
args.putString(
|
||||
PasswordStore.REQUEST_ARG_PATH,
|
||||
PasswordRepository.getRepositoryDirectory().absolutePath,
|
||||
PasswordRepository.getRepositoryDirectory().absolutePathString(),
|
||||
)
|
||||
|
||||
passwordList.arguments = args
|
||||
|
|
@ -60,7 +61,7 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
|
|||
}
|
||||
|
||||
private fun selectFolder() {
|
||||
intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePath)
|
||||
intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePathString())
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
|
|||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
|
@ -60,7 +61,11 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
|||
requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) {
|
||||
"Cannot navigate if ${PasswordStore.REQUEST_ARG_PATH} is not provided"
|
||||
}
|
||||
model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false)
|
||||
model.navigateTo(
|
||||
Paths.get(path),
|
||||
listMode = ListMode.DirectoriesOnly,
|
||||
pushPreviousLocation = false,
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
model.searchResult.flowWithLifecycle(lifecycle).collect { result ->
|
||||
recyclerAdapter.submitList(result.passwordItems)
|
||||
|
|
@ -88,7 +93,7 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
|||
}
|
||||
}
|
||||
|
||||
val currentDir: File
|
||||
val currentDir: Path
|
||||
get() = model.currentDir.value
|
||||
|
||||
interface OnFragmentInteractionListener {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ import com.github.michaelbull.result.onFailure
|
|||
import com.github.michaelbull.result.runCatching
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.name
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority.ERROR
|
||||
|
|
@ -220,17 +226,16 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
}
|
||||
|
||||
/** Clones the repository, the directory exists, deletes it */
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private fun cloneRepository() {
|
||||
val localDir =
|
||||
requireNotNull(PasswordRepository.getRepositoryDirectory()) {
|
||||
"Repository directory must be set before cloning"
|
||||
}
|
||||
val localDirFiles = localDir.listFiles() ?: emptyArray()
|
||||
val localDirFiles = if (localDir.exists()) localDir.listDirectoryEntries() else listOf()
|
||||
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
|
||||
if (
|
||||
localDir.exists() &&
|
||||
localDirFiles.isNotEmpty() &&
|
||||
!(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
|
||||
localDirFiles.isNotEmpty() && !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
|
||||
) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_delete_title)
|
||||
|
|
@ -246,7 +251,7 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
)
|
||||
withContext(dispatcherProvider.io()) {
|
||||
localDir.deleteRecursively()
|
||||
localDir.mkdirs()
|
||||
localDir.createDirectories()
|
||||
}
|
||||
snackbar.dismiss()
|
||||
launchGitOperation(GitOp.CLONE)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ import app.passwordstore.util.extensions.viewBinding
|
|||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.nio.file.LinkOption
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.notExists
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
|
@ -55,7 +59,9 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
|
|||
private fun createRepository() {
|
||||
val localDir = PasswordRepository.getRepositoryDirectory()
|
||||
runCatching {
|
||||
check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" }
|
||||
if (localDir.notExists(LinkOption.NOFOLLOW_LINKS)) {
|
||||
localDir.createDirectories()
|
||||
}
|
||||
PasswordRepository.createRepository(localDir)
|
||||
if (!PasswordRepository.isInitialized) {
|
||||
PasswordRepository.initialize()
|
||||
|
|
@ -64,7 +70,7 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
|
|||
}
|
||||
.onFailure { e ->
|
||||
logcat(ERROR) { e.asLog() }
|
||||
if (!localDir.delete()) {
|
||||
if (!localDir.deleteIfExists()) {
|
||||
logcat { "Failed to delete local repository: $localDir" }
|
||||
}
|
||||
finish()
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import app.passwordstore.util.extensions.viewBinding
|
|||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.writeText
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
|
|||
?: return@registerForActivityResult
|
||||
lifecycleScope.launch {
|
||||
withContext(dispatcherProvider.io()) {
|
||||
val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
|
||||
val gpgIdentifierFile = PasswordRepository.getRepositoryDirectory().resolve(".gpg-id")
|
||||
gpgIdentifierFile.writeText(selectedKey)
|
||||
}
|
||||
settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
|
||||
|
|
|
|||
|
|
@ -48,8 +48,13 @@ import com.github.michaelbull.result.fold
|
|||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlinx.coroutines.launch
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
||||
|
|
@ -66,7 +71,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
|
||||
private var recyclerViewStateToRestore: Parcelable? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
private var scrollTarget: File? = null
|
||||
private var scrollTarget: Path? = null
|
||||
|
||||
private val model: SearchableRepositoryViewModel by activityViewModels()
|
||||
private val binding by viewBinding(PasswordRecyclerViewBinding::bind)
|
||||
|
|
@ -76,7 +81,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
requireStore().refreshPasswordList()
|
||||
}
|
||||
|
||||
val currentDir: File
|
||||
val currentDir: Path
|
||||
get() = model.currentDir.value
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
@ -97,9 +102,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
}
|
||||
|
||||
private fun initializePasswordList() {
|
||||
val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git")
|
||||
val gitDir = PasswordRepository.getRepositoryDirectory().resolve(".git")
|
||||
val hasGitDir =
|
||||
gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true)
|
||||
gitDir.exists() && gitDir.isDirectory() && gitDir.listDirectoryEntries().isNotEmpty()
|
||||
binding.swipeRefresher.setOnRefreshListener {
|
||||
if (!hasGitDir) {
|
||||
requireStore().refreshPasswordList()
|
||||
|
|
@ -179,7 +184,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) {
|
||||
"Cannot navigate if ${PasswordStore.REQUEST_ARG_PATH} is not provided"
|
||||
}
|
||||
model.navigateTo(File(path), pushPreviousLocation = false)
|
||||
model.navigateTo(Paths.get(path), pushPreviousLocation = false)
|
||||
lifecycleScope.launch {
|
||||
model.searchResult.flowWithLifecycle(lifecycle).collect { result ->
|
||||
// Only run animations when the new list is filtered, i.e., the user submitted a search,
|
||||
|
|
@ -317,7 +322,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
val preferences =
|
||||
context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
|
||||
preferences.edit {
|
||||
putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
|
||||
putString(
|
||||
item.file.absolutePathString().base64(),
|
||||
System.currentTimeMillis().toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,7 +376,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
}
|
||||
}
|
||||
|
||||
fun navigateTo(file: File) {
|
||||
fun navigateTo(file: Path) {
|
||||
requireStore().clearSearch()
|
||||
model.navigateTo(
|
||||
file,
|
||||
|
|
@ -377,7 +385,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
fun scrollToOnNextRefresh(file: File) {
|
||||
fun scrollToOnNextRefresh(file: Path) {
|
||||
scrollTarget = file
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,13 +35,12 @@ import app.passwordstore.ui.git.base.BaseGitActivity
|
|||
import app.passwordstore.ui.onboarding.activity.OnboardingActivity
|
||||
import app.passwordstore.ui.settings.SettingsActivity
|
||||
import app.passwordstore.util.autofill.AutofillMatcher
|
||||
import app.passwordstore.util.extensions.asLog
|
||||
import app.passwordstore.util.extensions.base64
|
||||
import app.passwordstore.util.extensions.commitChange
|
||||
import app.passwordstore.util.extensions.contains
|
||||
import app.passwordstore.util.extensions.getString
|
||||
import app.passwordstore.util.extensions.isInsideRepository
|
||||
import app.passwordstore.util.extensions.launchActivity
|
||||
import app.passwordstore.util.extensions.listFilesRecursively
|
||||
import app.passwordstore.util.extensions.sharedPrefs
|
||||
import app.passwordstore.util.settings.AuthMode
|
||||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
|
|
@ -49,13 +48,29 @@ import app.passwordstore.util.shortcuts.ShortcutHandler
|
|||
import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
|
||||
import com.github.michaelbull.result.fold
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.lang.Character.UnicodeBlock
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.moveTo
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.nameWithoutExtension
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.relativeTo
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority.ERROR
|
||||
|
|
@ -88,13 +103,13 @@ class PasswordStore : BaseGitActivity() {
|
|||
"'Files' intent extra must be set"
|
||||
}
|
||||
val target =
|
||||
File(
|
||||
Paths.get(
|
||||
requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH")) {
|
||||
"'SELECTED_FOLDER_PATH' intent extra must be set"
|
||||
}
|
||||
)
|
||||
val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePath
|
||||
if (!target.isDirectory) {
|
||||
val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePathString()
|
||||
if (!target.isDirectory()) {
|
||||
logcat(ERROR) { "Tried moving passwords to a non-existing folder." }
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
|
@ -104,20 +119,21 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
lifecycleScope.launch(dispatcherProvider.io()) {
|
||||
for (file in filesToMove) {
|
||||
val source = File(file)
|
||||
val source = Paths.get(file)
|
||||
if (!source.exists()) {
|
||||
logcat(ERROR) { "Tried moving something that appears non-existent." }
|
||||
continue
|
||||
}
|
||||
val destinationFile = File(target.absolutePath + "/" + source.name)
|
||||
val destinationFile = Paths.get(target.absolutePathString(), source.name)
|
||||
val basename = source.nameWithoutExtension
|
||||
val sourceLongName =
|
||||
getLongName(
|
||||
requireNotNull(source.parent) { "$file has no parent" },
|
||||
requireNotNull(source.parent) { "$file has no parent" }.absolutePathString(),
|
||||
repositoryPath,
|
||||
basename,
|
||||
)
|
||||
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
|
||||
val destinationLongName =
|
||||
getLongName(target.absolutePathString(), repositoryPath, basename)
|
||||
if (destinationFile.exists()) {
|
||||
logcat(ERROR) { "Trying to move a file that already exists." }
|
||||
withContext(dispatcherProvider.main()) {
|
||||
|
|
@ -142,15 +158,16 @@ class PasswordStore : BaseGitActivity() {
|
|||
}
|
||||
when (filesToMove.size) {
|
||||
1 -> {
|
||||
val source = File(filesToMove[0])
|
||||
val source = Paths.get(filesToMove[0])
|
||||
val basename = source.nameWithoutExtension
|
||||
val sourceLongName =
|
||||
getLongName(
|
||||
requireNotNull(source.parent) { "$basename has no parent" },
|
||||
requireNotNull(source.parent) { "$basename has no parent" }.pathString,
|
||||
repositoryPath,
|
||||
basename,
|
||||
)
|
||||
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
|
||||
val destinationLongName =
|
||||
getLongName(target.absolutePathString(), repositoryPath, basename)
|
||||
withContext(dispatcherProvider.main()) {
|
||||
commitChange(
|
||||
resources.getString(
|
||||
|
|
@ -162,8 +179,8 @@ class PasswordStore : BaseGitActivity() {
|
|||
}
|
||||
}
|
||||
else -> {
|
||||
val repoDir = PasswordRepository.getRepositoryDirectory().absolutePath
|
||||
val relativePath = getRelativePath("${target.absolutePath}/", repoDir)
|
||||
val repoDir = PasswordRepository.getRepositoryDirectory().absolutePathString()
|
||||
val relativePath = getRelativePath("${target.absolutePathString()}/", repoDir)
|
||||
withContext(dispatcherProvider.main()) {
|
||||
commitChange(
|
||||
resources.getString(R.string.git_commit_move_multiple_text, relativePath)
|
||||
|
|
@ -203,7 +220,7 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
lifecycleScope.launch {
|
||||
model.currentDir.flowWithLifecycle(lifecycle).collect { dir ->
|
||||
val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
|
||||
val basePath = PasswordRepository.getRepositoryDirectory().absolute()
|
||||
supportActionBar?.apply {
|
||||
if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
|
||||
}
|
||||
|
|
@ -348,7 +365,7 @@ class PasswordStore : BaseGitActivity() {
|
|||
checkLocalRepository(PasswordRepository.getRepositoryDirectory())
|
||||
}
|
||||
|
||||
private fun checkLocalRepository(localDir: File?) {
|
||||
private fun checkLocalRepository(localDir: Path?) {
|
||||
if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) {
|
||||
// do not push the fragment if we already have it
|
||||
if (
|
||||
|
|
@ -356,7 +373,10 @@ class PasswordStore : BaseGitActivity() {
|
|||
) {
|
||||
settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) }
|
||||
val args = Bundle()
|
||||
args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
|
||||
args.putString(
|
||||
REQUEST_ARG_PATH,
|
||||
PasswordRepository.getRepositoryDirectory().absolutePathString(),
|
||||
)
|
||||
|
||||
// if the activity was started from the autofill settings, the
|
||||
// intent is to match a clicked pwd with app. pass this to fragment
|
||||
|
|
@ -406,25 +426,27 @@ class PasswordStore : BaseGitActivity() {
|
|||
fun createPassword() {
|
||||
if (!validateState()) return
|
||||
val currentDir = currentDir
|
||||
logcat(INFO) { "Adding file to : ${currentDir.absolutePath}" }
|
||||
logcat(INFO) { "Adding file to : ${currentDir.absolutePathString()}" }
|
||||
val intent = Intent(this, PasswordCreationActivity::class.java)
|
||||
intent.putExtra(BasePGPActivity.EXTRA_FILE_PATH, currentDir.absolutePath)
|
||||
intent.putExtra(BasePGPActivity.EXTRA_FILE_PATH, currentDir.absolutePathString())
|
||||
intent.putExtra(
|
||||
BasePGPActivity.EXTRA_REPO_PATH,
|
||||
PasswordRepository.getRepositoryDirectory().absolutePath,
|
||||
PasswordRepository.getRepositoryDirectory().absolutePathString(),
|
||||
)
|
||||
listRefreshAction.launch(intent)
|
||||
}
|
||||
|
||||
fun createFolder() {
|
||||
if (!validateState()) return
|
||||
FolderCreationDialogFragment.newInstance(currentDir.path).show(supportFragmentManager, null)
|
||||
FolderCreationDialogFragment.newInstance(currentDir.pathString)
|
||||
.show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
fun deletePasswords(selectedItems: List<PasswordItem>) {
|
||||
var size = 0
|
||||
selectedItems.forEach {
|
||||
if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size
|
||||
if (it.file.isRegularFile()) size++ else size += it.file.listDirectoryEntries().size
|
||||
}
|
||||
if (size == 0) {
|
||||
selectedItems.map { item -> item.file.deleteRecursively() }
|
||||
|
|
@ -434,9 +456,9 @@ class PasswordStore : BaseGitActivity() {
|
|||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(resources.getQuantityString(R.plurals.delete_dialog_text, size, size))
|
||||
.setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ ->
|
||||
val filesToDelete = arrayListOf<File>()
|
||||
val filesToDelete = arrayListOf<Path>()
|
||||
selectedItems.forEach { item ->
|
||||
if (item.file.isDirectory) filesToDelete.addAll(item.file.listFilesRecursively())
|
||||
if (item.file.isDirectory()) filesToDelete.addAll(item.file.listDirectoryEntries())
|
||||
else filesToDelete.add(item.file)
|
||||
}
|
||||
selectedItems.map { item -> item.file.deleteRecursively() }
|
||||
|
|
@ -444,7 +466,7 @@ class PasswordStore : BaseGitActivity() {
|
|||
AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete)
|
||||
val fmt =
|
||||
selectedItems.joinToString(separator = ", ") { item ->
|
||||
item.file.toRelativeString(PasswordRepository.getRepositoryDirectory())
|
||||
item.file.relativeTo(PasswordRepository.getRepositoryDirectory()).absolutePathString()
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
commitChange(resources.getString(R.string.git_commit_remove_text, fmt))
|
||||
|
|
@ -456,7 +478,7 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
fun movePasswords(values: List<PasswordItem>) {
|
||||
val intent = Intent(this, SelectFolderActivity::class.java)
|
||||
val fileLocations = values.map { it.file.absolutePath }.toTypedArray()
|
||||
val fileLocations = values.map { it.file.absolutePathString() }.toTypedArray()
|
||||
intent.putExtra("Files", fileLocations)
|
||||
passwordMoveAction.launch(intent)
|
||||
}
|
||||
|
|
@ -497,7 +519,7 @@ class PasswordStore : BaseGitActivity() {
|
|||
.setView(view)
|
||||
.setMessage(getString(R.string.message_rename_folder, oldCategory.name))
|
||||
.setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||
val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}")
|
||||
val newCategory = Paths.get("${oldCategory.file.parent}/${newCategoryEditText.text}")
|
||||
when {
|
||||
newCategoryEditText.text.isNullOrBlank() ->
|
||||
renameCategory(oldCategory, CategoryRenameError.EmptyField)
|
||||
|
|
@ -512,11 +534,11 @@ class PasswordStore : BaseGitActivity() {
|
|||
// history
|
||||
val preference =
|
||||
getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
|
||||
val timestamp = preference.getString(oldCategory.file.absolutePath.base64())
|
||||
val timestamp = preference.getString(oldCategory.file.absolutePathString().base64())
|
||||
if (timestamp != null) {
|
||||
preference.edit {
|
||||
remove(oldCategory.file.absolutePath.base64())
|
||||
putString(newCategory.absolutePath.base64(), timestamp)
|
||||
remove(oldCategory.file.absolutePathString().base64())
|
||||
putString(newCategory.absolutePathString().base64(), timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -552,14 +574,14 @@ class PasswordStore : BaseGitActivity() {
|
|||
* entered if it is a directory or scrolled into view if it is a file (both inside the current
|
||||
* directory).
|
||||
*/
|
||||
fun refreshPasswordList(target: File? = null) {
|
||||
fun refreshPasswordList(target: Path? = null) {
|
||||
val plist = getPasswordFragment()
|
||||
if (target?.isDirectory == true && model.currentDir.value.contains(target)) {
|
||||
if (target?.isDirectory() == true && model.currentDir.value.contains(target)) {
|
||||
plist?.navigateTo(target)
|
||||
} else if (target?.isFile == true && model.currentDir.value.contains(target)) {
|
||||
} else if (target?.isRegularFile() == true && model.currentDir.value.contains(target)) {
|
||||
// Creating new passwords is handled by an activity, so we will refresh in onStart.
|
||||
plist?.scrollToOnNextRefresh(target)
|
||||
} else if (model.currentDir.value.isDirectory) {
|
||||
} else if (model.currentDir.value.isDirectory()) {
|
||||
model.forceRefresh()
|
||||
} else {
|
||||
model.reset()
|
||||
|
|
@ -567,40 +589,41 @@ class PasswordStore : BaseGitActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private val currentDir: File
|
||||
private val currentDir: Path
|
||||
get() = getPasswordFragment()?.currentDir ?: PasswordRepository.getRepositoryDirectory()
|
||||
|
||||
private suspend fun moveFile(source: File, destinationFile: File) {
|
||||
private suspend fun moveFile(source: Path, destinationFile: Path) {
|
||||
val sourceDestinationMap =
|
||||
if (source.isDirectory) {
|
||||
destinationFile.mkdirs()
|
||||
if (source.isDirectory()) {
|
||||
destinationFile.createDirectories()
|
||||
// Recursively list all files (not directories) below `source`, then
|
||||
// obtain the corresponding target file by resolving the relative path
|
||||
// starting at the destination folder.
|
||||
source.listFilesRecursively().associateWith {
|
||||
source.listDirectoryEntries().associateWith {
|
||||
destinationFile.resolve(it.relativeTo(source))
|
||||
}
|
||||
} else {
|
||||
mapOf(source to destinationFile)
|
||||
}
|
||||
if (!source.renameTo(destinationFile)) {
|
||||
logcat(ERROR) { "Something went wrong while moving $source to $destinationFile." }
|
||||
withContext(dispatcherProvider.main()) {
|
||||
MaterialAlertDialogBuilder(this@PasswordStore)
|
||||
.setTitle(R.string.password_move_error_title)
|
||||
.setMessage(getString(R.string.password_move_error_message, source, destinationFile))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
runCatching { source.moveTo(destinationFile) }
|
||||
.onFailure {
|
||||
logcat(ERROR) { it.asLog("Something went wrong while moving $source to $destinationFile.") }
|
||||
withContext(dispatcherProvider.main()) {
|
||||
MaterialAlertDialogBuilder(this@PasswordStore)
|
||||
.setTitle(R.string.password_move_error_title)
|
||||
.setMessage(getString(R.string.password_move_error_message, source, destinationFile))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AutofillMatcher.updateMatches(this, sourceDestinationMap)
|
||||
}
|
||||
.onSuccess { AutofillMatcher.updateMatches(this, sourceDestinationMap) }
|
||||
}
|
||||
|
||||
fun matchPasswordWithApp(item: PasswordItem) {
|
||||
val path =
|
||||
item.file.absolutePath
|
||||
item.file
|
||||
.absolutePathString()
|
||||
.replace(PasswordRepository.getRepositoryDirectory().toString() + "/", "")
|
||||
.replace(".gpg", "")
|
||||
val data = Intent()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ import de.Maxr1998.modernpreferences.PreferenceScreen
|
|||
import de.Maxr1998.modernpreferences.helpers.onClick
|
||||
import de.Maxr1998.modernpreferences.helpers.pref
|
||||
import de.Maxr1998.modernpreferences.helpers.switch
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteRecursively
|
||||
|
||||
class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider {
|
||||
|
||||
|
|
@ -58,6 +61,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
|
|||
|
||||
private var showSshKeyPref: Preference? = null
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
override fun provideSettings(builder: PreferenceScreen.Builder) {
|
||||
val encryptedPreferences = hiltEntryPoint.encryptedPreferences()
|
||||
val gitSettings = hiltEntryPoint.gitSettings()
|
||||
|
|
@ -164,7 +168,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
|
|||
PasswordRepository.closeRepository()
|
||||
PasswordRepository.getRepositoryDirectory().let { dir ->
|
||||
dir.deleteRecursively()
|
||||
dir.mkdirs()
|
||||
dir.createDirectories()
|
||||
}
|
||||
}
|
||||
.onFailure { it.message?.let { message -> activity.snackbar(message = message) } }
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import com.github.androidpasswordstore.autofillparser.AutofillAction
|
|||
import com.github.androidpasswordstore.autofillparser.FillableForm
|
||||
import com.github.androidpasswordstore.autofillparser.fillWith
|
||||
import com.github.michaelbull.result.fold
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
|
@ -58,7 +58,7 @@ class Api26AutofillResponseBuilder private constructor(form: FillableForm) :
|
|||
}
|
||||
}
|
||||
|
||||
private fun makeMatchDataset(context: Context, file: File): Dataset? {
|
||||
private fun makeMatchDataset(context: Context, file: Path): Dataset? {
|
||||
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
|
||||
val metadata = makeFillMatchMetadata(context, file)
|
||||
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
|
|
@ -135,7 +135,7 @@ class Api26AutofillResponseBuilder private constructor(form: FillableForm) :
|
|||
}
|
||||
}
|
||||
|
||||
private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? {
|
||||
private fun makeFillResponse(context: Context, matchedFiles: List<Path>): FillResponse? {
|
||||
var datasetCount = 0
|
||||
return FillResponse.Builder().run {
|
||||
for (file in matchedFiles) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import com.github.androidpasswordstore.autofillparser.AutofillAction
|
|||
import com.github.androidpasswordstore.autofillparser.FillableForm
|
||||
import com.github.androidpasswordstore.autofillparser.fillWith
|
||||
import com.github.michaelbull.result.fold
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
|
@ -123,7 +123,7 @@ class Api30AutofillResponseBuilder private constructor(form: FillableForm) :
|
|||
|
||||
private fun makeMatchDataset(
|
||||
context: Context,
|
||||
file: File,
|
||||
file: Path,
|
||||
imeSpec: InlinePresentationSpec?,
|
||||
): Dataset? {
|
||||
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
|
||||
|
|
@ -199,7 +199,7 @@ class Api30AutofillResponseBuilder private constructor(form: FillableForm) :
|
|||
private fun makeFillResponse(
|
||||
context: Context,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
matchedFiles: List<File>,
|
||||
matchedFiles: List<Path>,
|
||||
): FillResponse? {
|
||||
var datasetCount = 0
|
||||
val imeSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ import com.github.androidpasswordstore.autofillparser.computeCertificatesHash
|
|||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.LogPriority.WARN
|
||||
import logcat.logcat
|
||||
|
|
@ -103,19 +107,22 @@ class AutofillMatcher {
|
|||
fun getMatchesFor(
|
||||
context: Context,
|
||||
formOrigin: FormOrigin,
|
||||
): Result<List<File>, AutofillPublisherChangedException> {
|
||||
): Result<List<Path>, AutofillPublisherChangedException> {
|
||||
if (hasFormOriginHashChanged(context, formOrigin)) {
|
||||
return Err(AutofillPublisherChangedException(formOrigin))
|
||||
}
|
||||
val matchPreferences = context.matchPreferences(formOrigin)
|
||||
val matchedFiles =
|
||||
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
|
||||
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { Paths.get(it) }
|
||||
return Ok(
|
||||
matchedFiles
|
||||
.filter { it.exists() }
|
||||
.also { validFiles ->
|
||||
matchPreferences.edit {
|
||||
putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet())
|
||||
putStringSet(
|
||||
matchesKey(formOrigin),
|
||||
validFiles.map { it.absolutePathString() }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -135,7 +142,7 @@ class AutofillMatcher {
|
|||
* The maximum number of matches is limited by [MAX_NUM_MATCHES] since older versions of Android
|
||||
* may crash when too many datasets are offered.
|
||||
*/
|
||||
fun addMatchFor(context: Context, formOrigin: FormOrigin, file: File) {
|
||||
fun addMatchFor(context: Context, formOrigin: FormOrigin, file: Path) {
|
||||
if (!file.exists()) return
|
||||
if (hasFormOriginHashChanged(context, formOrigin)) {
|
||||
// This should never happen since we already verified the publisher in
|
||||
|
|
@ -145,8 +152,8 @@ class AutofillMatcher {
|
|||
}
|
||||
val matchPreferences = context.matchPreferences(formOrigin)
|
||||
val matchedFiles =
|
||||
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
|
||||
val newFiles = setOf(file.absoluteFile).union(matchedFiles)
|
||||
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet()).orEmpty().map(Paths::get)
|
||||
val newFiles = setOf(file.absolute()).union(matchedFiles)
|
||||
if (newFiles.size > MAX_NUM_MATCHES) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
|
|
@ -157,7 +164,7 @@ class AutofillMatcher {
|
|||
return
|
||||
}
|
||||
matchPreferences.edit {
|
||||
putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet())
|
||||
putStringSet(matchesKey(formOrigin), newFiles.map(Path::absolutePathString).toSet())
|
||||
}
|
||||
storeFormOriginHash(context, formOrigin)
|
||||
logcat { "Stored match for $formOrigin" }
|
||||
|
|
@ -169,12 +176,14 @@ class AutofillMatcher {
|
|||
*/
|
||||
fun updateMatches(
|
||||
context: Context,
|
||||
moveFromTo: Map<File, File> = emptyMap(),
|
||||
delete: Collection<File> = emptyList(),
|
||||
moveFromTo: Map<Path, Path> = emptyMap(),
|
||||
delete: Collection<Path> = emptyList(),
|
||||
) {
|
||||
val deletePathList = delete.map { it.absolutePath }
|
||||
val deletePathList = delete.map { it.absolutePathString() }
|
||||
val oldNewPathMap =
|
||||
moveFromTo.mapValues { it.value.absolutePath }.mapKeys { it.key.absolutePath }
|
||||
moveFromTo
|
||||
.mapValues { it.value.absolutePathString() }
|
||||
.mapKeys { it.key.absolutePathString() }
|
||||
for (prefs in listOf(context.autofillAppMatches, context.autofillWebMatches)) {
|
||||
for ((key, value) in prefs.all) {
|
||||
if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue
|
||||
|
|
@ -190,8 +199,8 @@ class AutofillMatcher {
|
|||
val newMatches =
|
||||
oldMatches
|
||||
.asSequence()
|
||||
.minus(deletePathList)
|
||||
.minus(oldNewPathMap.values)
|
||||
.minus(deletePathList.toSet())
|
||||
.minus(oldNewPathMap.values.toSet())
|
||||
.map { match ->
|
||||
val newPath = oldNewPathMap[match] ?: return@map match
|
||||
logcat { "Updating match for $key: $match --> $newPath" }
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import app.passwordstore.util.extensions.sharedPrefs
|
|||
import app.passwordstore.util.services.getDefaultUsername
|
||||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.github.androidpasswordstore.autofillparser.Credentials
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.nameWithoutExtension
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
enum class DirectoryStructure(val value: String) {
|
||||
EncryptedUsername("encrypted_username"),
|
||||
|
|
@ -29,11 +32,11 @@ enum class DirectoryStructure(val value: String) {
|
|||
* - work/example.org/john@doe.org/password.gpg --> john@doe.org (DirectoryBased)
|
||||
* - Temporary PIN.gpg --> Temporary PIN (DirectoryBased, fallback)
|
||||
*/
|
||||
fun getUsernameFor(file: File): String? =
|
||||
fun getUsernameFor(file: Path): String? =
|
||||
when (this) {
|
||||
EncryptedUsername -> null
|
||||
FileBased -> file.nameWithoutExtension
|
||||
DirectoryBased -> file.parentFile?.name ?: file.nameWithoutExtension
|
||||
DirectoryBased -> file.parent?.name ?: file.nameWithoutExtension
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,11 +53,11 @@ enum class DirectoryStructure(val value: String) {
|
|||
* - work/example.org/john@doe.org/password.gpg --> example.org (DirectoryBased)
|
||||
* - Temporary PIN.gpg --> null (DirectoryBased)
|
||||
*/
|
||||
fun getIdentifierFor(file: File): String? =
|
||||
fun getIdentifierFor(file: Path): String? =
|
||||
when (this) {
|
||||
EncryptedUsername -> file.nameWithoutExtension
|
||||
FileBased -> file.parentFile?.name ?: file.nameWithoutExtension
|
||||
DirectoryBased -> file.parentFile?.parent
|
||||
FileBased -> file.parent?.name ?: file.nameWithoutExtension
|
||||
DirectoryBased -> file.parent?.parent?.pathString
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,11 +72,11 @@ enum class DirectoryStructure(val value: String) {
|
|||
* - work/example.org/john@doe.org/password.gpg --> work (DirectoryBased)
|
||||
* - example.org/john@doe.org/password.gpg --> null (DirectoryBased)
|
||||
*/
|
||||
fun getPathToIdentifierFor(file: File): String? =
|
||||
fun getPathToIdentifierFor(file: Path): String? =
|
||||
when (this) {
|
||||
EncryptedUsername -> file.parent
|
||||
FileBased -> file.parentFile?.parent
|
||||
DirectoryBased -> file.parentFile?.parentFile?.parent
|
||||
EncryptedUsername -> file.parent.pathString
|
||||
FileBased -> file.parent?.parent?.pathString
|
||||
DirectoryBased -> file.parent?.parent?.parent?.pathString
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,12 +93,12 @@ enum class DirectoryStructure(val value: String) {
|
|||
* - work/example.org/john@doe.org/password.gpg --> john@doe.org/password (DirectoryBased)
|
||||
* - Temporary PIN.gpg --> Temporary PIN (DirectoryBased, fallback)
|
||||
*/
|
||||
fun getAccountPartFor(file: File): String? =
|
||||
fun getAccountPartFor(file: Path): String? =
|
||||
when (this) {
|
||||
EncryptedUsername -> null
|
||||
FileBased -> file.nameWithoutExtension.takeIf { file.parentFile != null }
|
||||
FileBased -> file.nameWithoutExtension.takeIf { file.parent != null }
|
||||
DirectoryBased ->
|
||||
file.parentFile?.let { parentFile -> "${parentFile.name}/${file.nameWithoutExtension}" }
|
||||
file.parent?.let { parent -> "${parent.name}/${file.nameWithoutExtension}" }
|
||||
?: file.nameWithoutExtension
|
||||
}
|
||||
|
||||
|
|
@ -132,13 +135,13 @@ object AutofillPreferences {
|
|||
|
||||
fun credentialsFromStoreEntry(
|
||||
context: Context,
|
||||
file: File,
|
||||
path: Path,
|
||||
entry: PasswordEntry,
|
||||
directoryStructure: DirectoryStructure,
|
||||
): Credentials {
|
||||
// Always give priority to a username stored in the encrypted extras
|
||||
val username =
|
||||
entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername()
|
||||
entry.username ?: directoryStructure.getUsernameFor(path) ?: context.getDefaultUsername()
|
||||
val totp = if (entry.hasTotp()) entry.currentOtp else null
|
||||
return Credentials(username, entry.password, totp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ import androidx.autofill.inline.v1.InlineSuggestionUi
|
|||
import app.passwordstore.R
|
||||
import app.passwordstore.data.repo.PasswordRepository
|
||||
import app.passwordstore.ui.passwords.PasswordStore
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.relativeTo
|
||||
|
||||
data class DatasetMetadata(val title: String, val subtitle: String?, @DrawableRes val iconRes: Int)
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ fun makeInlinePresentation(
|
|||
return InlinePresentation(slice, imeSpec, false)
|
||||
}
|
||||
|
||||
fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata {
|
||||
fun makeFillMatchMetadata(context: Context, file: Path): DatasetMetadata {
|
||||
val directoryStructure = AutofillPreferences.directoryStructure(context)
|
||||
val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
|
||||
val title =
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
package app.passwordstore.util.extensions
|
||||
|
||||
import app.passwordstore.data.repo.PasswordRepository
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
import kotlin.io.path.absolutePathString
|
||||
import logcat.asLog
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
|
@ -18,30 +17,15 @@ infix fun Int.hasFlag(flag: Int): Boolean {
|
|||
return this and flag == flag
|
||||
}
|
||||
|
||||
/** Checks whether this [File] is a directory that contains [other]. */
|
||||
fun File.contains(other: File): Boolean {
|
||||
if (!isDirectory) return false
|
||||
if (!other.exists()) return false
|
||||
val relativePath =
|
||||
runCatching { other.relativeTo(this) }
|
||||
.getOrElse {
|
||||
return false
|
||||
}
|
||||
// Direct containment is equivalent to the relative path being equal to the filename.
|
||||
return relativePath.path == other.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this [File] is in the password repository directory as given by
|
||||
* Checks if this [Path] is in the password repository directory as given by
|
||||
* [PasswordRepository.getRepositoryDirectory]
|
||||
*/
|
||||
fun File.isInsideRepository(): Boolean {
|
||||
return canonicalPath.contains(PasswordRepository.getRepositoryDirectory().canonicalPath)
|
||||
fun Path.isInsideRepository(): Boolean {
|
||||
return absolutePathString()
|
||||
.contains(PasswordRepository.getRepositoryDirectory().absolutePathString())
|
||||
}
|
||||
|
||||
/** Recursively lists the files in this [File], skipping any directories it encounters. */
|
||||
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
|
||||
|
||||
/**
|
||||
* Unique SHA-1 hash of this commit as hexadecimal string.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
|
||||
/** Whether the operation requires authentication or not. */
|
||||
open val requiresAuth: Boolean = true
|
||||
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
|
||||
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key").toPath()
|
||||
private var sshSessionFactory: SshjSessionFactory? = null
|
||||
private val hiltEntryPoint =
|
||||
EntryPointAccessors.fromApplication<GitOperationEntryPoint>(callingActivity)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import app.passwordstore.util.extensions.unsafeLazy
|
|||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPairGenerator
|
||||
|
|
@ -34,6 +33,12 @@ import java.security.PrivateKey
|
|||
import java.security.PublicKey
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.io.path.writeText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -113,10 +118,10 @@ object SshKey {
|
|||
get() = Application.instance.applicationContext
|
||||
|
||||
private val privateKeyFile
|
||||
get() = File(context.filesDir, ".ssh_key")
|
||||
get() = context.filesDir.toPath().resolve(".ssh_key")
|
||||
|
||||
private val publicKeyFile
|
||||
get() = File(context.filesDir, ".ssh_key.pub")
|
||||
get() = context.filesDir.toPath().resolve(".ssh_key.pub")
|
||||
|
||||
private var type: Type?
|
||||
get() = Type.fromValue(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_KEY_TYPE))
|
||||
|
|
@ -178,11 +183,11 @@ object SshKey {
|
|||
context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit {
|
||||
clear()
|
||||
}
|
||||
if (privateKeyFile.isFile) {
|
||||
privateKeyFile.delete()
|
||||
if (privateKeyFile.isRegularFile()) {
|
||||
privateKeyFile.deleteIfExists()
|
||||
}
|
||||
if (publicKeyFile.isFile) {
|
||||
publicKeyFile.delete()
|
||||
if (publicKeyFile.isRegularFile()) {
|
||||
publicKeyFile.deleteIfExists()
|
||||
}
|
||||
context.getEncryptedGitPrefs().edit { remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) }
|
||||
type = null
|
||||
|
|
@ -247,7 +252,7 @@ object SshKey {
|
|||
withContext(Dispatchers.IO) {
|
||||
EncryptedFile.Builder(
|
||||
context,
|
||||
privateKeyFile,
|
||||
privateKeyFile.toFile(),
|
||||
getOrCreateWrappingMasterKey(requireAuthentication),
|
||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
|
||||
)
|
||||
|
|
@ -304,7 +309,7 @@ object SshKey {
|
|||
fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? =
|
||||
when (type) {
|
||||
Type.LegacyGenerated,
|
||||
Type.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
|
||||
Type.Imported -> client.loadKeys(privateKeyFile.absolutePathString(), passphraseFinder)
|
||||
Type.KeystoreNative -> KeystoreNativeKeyProvider
|
||||
Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider
|
||||
null -> null
|
||||
|
|
|
|||
|
|
@ -11,15 +11,18 @@ import app.passwordstore.util.git.operation.CredentialFinder
|
|||
import app.passwordstore.util.settings.AuthMode
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Path
|
||||
import java.security.PublicKey
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.io.path.writeText
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import logcat.LogPriority.WARN
|
||||
import logcat.logcat
|
||||
|
|
@ -69,7 +72,7 @@ abstract class InteractivePasswordFinder(private val dispatcherProvider: Dispatc
|
|||
|
||||
class SshjSessionFactory(
|
||||
private val authMethod: SshAuthMethod,
|
||||
private val hostKeyFile: File,
|
||||
private val hostKeyFile: Path,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
) : SshSessionFactory() {
|
||||
|
||||
|
|
@ -93,7 +96,7 @@ class SshjSessionFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
|
||||
private fun makeTofuHostKeyVerifier(hostKeyFile: Path): HostKeyVerifier {
|
||||
if (!hostKeyFile.exists()) {
|
||||
return object : HostKeyVerifier {
|
||||
override fun verify(hostname: String?, port: Int, key: PublicKey?): Boolean {
|
||||
|
|
@ -125,7 +128,7 @@ private class SshjSession(
|
|||
uri: URIish,
|
||||
private val username: String,
|
||||
private val authMethod: SshAuthMethod,
|
||||
private val hostKeyFile: File,
|
||||
private val hostKeyFile: Path,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
) : RemoteSession {
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import app.passwordstore.R
|
|||
import app.passwordstore.data.repo.PasswordRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.io.path.pathString
|
||||
import logcat.logcat
|
||||
|
||||
class PasswordExportService : Service() {
|
||||
|
|
@ -61,9 +62,9 @@ class PasswordExportService : Service() {
|
|||
requireNotNull(PasswordRepository.getRepositoryDirectory()) {
|
||||
"Password directory must be set to export them"
|
||||
}
|
||||
val sourcePassDir = DocumentFile.fromFile(repositoryDirectory)
|
||||
val sourcePassDir = DocumentFile.fromFile(repositoryDirectory.toFile())
|
||||
|
||||
logcat { "Copying ${repositoryDirectory.path} to $targetDirectory" }
|
||||
logcat { "Copying ${repositoryDirectory.pathString} to $targetDirectory" }
|
||||
|
||||
val dateString = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
val passDir = targetDirectory.createDirectory("password_store_$dateString")
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ import app.passwordstore.injection.prefs.SettingsPreferences
|
|||
import app.passwordstore.util.extensions.getString
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import org.eclipse.jgit.transport.URIish
|
||||
|
||||
enum class Protocol(val pref: String) {
|
||||
|
|
@ -174,9 +176,9 @@ constructor(
|
|||
|
||||
/** Deletes a previously saved SSH host key */
|
||||
fun clearSavedHostKey() {
|
||||
File(hostKeyPath).delete()
|
||||
Paths.get(hostKeyPath).deleteIfExists()
|
||||
}
|
||||
|
||||
/** Returns true if a host key was previously saved */
|
||||
fun hasSavedHostKey(): Boolean = File(hostKeyPath).exists()
|
||||
fun hasSavedHostKey(): Boolean = Paths.get(hostKeyPath).exists()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ import app.passwordstore.util.extensions.getString
|
|||
import app.passwordstore.util.git.sshj.SshKey
|
||||
import com.github.michaelbull.result.get
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.exists
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.LogPriority.INFO
|
||||
import logcat.logcat
|
||||
|
|
@ -108,7 +109,7 @@ private fun migrateToHideAll(sharedPrefs: SharedPreferences) {
|
|||
}
|
||||
|
||||
private fun migrateToSshKey(filesDirPath: String, sharedPrefs: SharedPreferences) {
|
||||
val privateKeyFile = File(filesDirPath, ".ssh_key")
|
||||
val privateKeyFile = Paths.get(filesDirPath, ".ssh_key")
|
||||
if (
|
||||
sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) &&
|
||||
!SshKey.exists &&
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import app.passwordstore.Application
|
|||
import app.passwordstore.data.password.PasswordItem
|
||||
import app.passwordstore.util.extensions.base64
|
||||
import app.passwordstore.util.extensions.getString
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>) {
|
||||
FOLDER_FIRST(
|
||||
|
|
@ -27,8 +28,8 @@ enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>)
|
|||
Comparator { p1: PasswordItem, p2: PasswordItem ->
|
||||
val recentHistory =
|
||||
Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
|
||||
val timeP1 = recentHistory.getString(p1.file.absolutePath.base64())
|
||||
val timeP2 = recentHistory.getString(p2.file.absolutePath.base64())
|
||||
val timeP1 = recentHistory.getString(p1.file.absolutePathString().base64())
|
||||
val timeP2 = recentHistory.getString(p2.file.absolutePathString().base64())
|
||||
when {
|
||||
timeP1 != null && timeP2 != null -> timeP2.compareTo(timeP1)
|
||||
timeP1 != null && timeP2 == null -> return@Comparator -1
|
||||
|
|
|
|||
|
|
@ -32,10 +32,21 @@ import app.passwordstore.util.settings.PasswordSortOrder
|
|||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.github.androidpasswordstore.sublimefuzzy.Fuzzy
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.isHidden
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.relativeTo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -43,7 +54,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -54,9 +64,9 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.yield
|
||||
import me.zhanghai.android.fastscroll.PopupTextProvider
|
||||
|
||||
private fun File.toPasswordItem() =
|
||||
if (isFile) PasswordItem.newPassword(name, this, PasswordRepository.getRepositoryDirectory())
|
||||
else PasswordItem.newCategory(name, this, PasswordRepository.getRepositoryDirectory())
|
||||
private fun Path.toPasswordItem() =
|
||||
if (isRegularFile()) PasswordItem.newPassword(this, PasswordRepository.getRepositoryDirectory())
|
||||
else PasswordItem.newCategory(this, PasswordRepository.getRepositoryDirectory())
|
||||
|
||||
private fun PasswordItem.fuzzyMatch(filter: String): Int {
|
||||
val (_, score) = Fuzzy.fuzzyMatch(filter, longName)
|
||||
|
|
@ -89,7 +99,7 @@ private fun PasswordItem.Companion.makeComparator(
|
|||
}
|
||||
|
||||
val PasswordItem.stableId: String
|
||||
get() = file.absolutePath
|
||||
get() = file.absolutePathString()
|
||||
|
||||
enum class FilterMode {
|
||||
NoFilter,
|
||||
|
|
@ -151,7 +161,7 @@ constructor(
|
|||
get() = PasswordItem.makeComparator(typeSortOrder, directoryStructure)
|
||||
|
||||
private data class SearchAction(
|
||||
val baseDirectory: File,
|
||||
val baseDirectory: Path,
|
||||
val filter: String,
|
||||
val filterMode: FilterMode,
|
||||
val searchMode: SearchMode,
|
||||
|
|
@ -162,7 +172,7 @@ constructor(
|
|||
)
|
||||
|
||||
private fun makeSearchAction(
|
||||
baseDirectory: File,
|
||||
baseDirectory: Path,
|
||||
filter: String,
|
||||
filterMode: FilterMode,
|
||||
searchMode: SearchMode,
|
||||
|
|
@ -206,9 +216,9 @@ constructor(
|
|||
val prefilteredResultFlow =
|
||||
when (searchAction.listMode) {
|
||||
ListMode.FilesOnly ->
|
||||
listResultFlow.filter { it.isFile }.flowOn(dispatcherProvider.io())
|
||||
listResultFlow.filter { it.isRegularFile() }.flowOn(dispatcherProvider.io())
|
||||
ListMode.DirectoriesOnly ->
|
||||
listResultFlow.filter { it.isDirectory }.flowOn(dispatcherProvider.io())
|
||||
listResultFlow.filter { it.isDirectory() }.flowOn(dispatcherProvider.io())
|
||||
ListMode.AllEntries -> listResultFlow
|
||||
}
|
||||
val passwordList =
|
||||
|
|
@ -223,7 +233,7 @@ constructor(
|
|||
FilterMode.Exact -> {
|
||||
prefilteredResultFlow
|
||||
.filter { absoluteFile ->
|
||||
absoluteFile.relativeTo(root).path.contains(searchAction.filter)
|
||||
absoluteFile.relativeTo(root).pathString.contains(searchAction.filter)
|
||||
}
|
||||
.map { it.toPasswordItem() }
|
||||
.flowOn(dispatcherProvider.io())
|
||||
|
|
@ -238,7 +248,7 @@ constructor(
|
|||
if (regex != null) {
|
||||
prefilteredResultFlow
|
||||
.filter { absoluteFile ->
|
||||
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
|
||||
regex.containsMatchIn(absoluteFile.relativeTo(root).pathString)
|
||||
}
|
||||
.map { it.toPasswordItem() }
|
||||
.flowOn(dispatcherProvider.io())
|
||||
|
|
@ -268,27 +278,28 @@ constructor(
|
|||
}
|
||||
.flowOn(dispatcherProvider.io())
|
||||
|
||||
private fun shouldTake(file: File) =
|
||||
private fun shouldTake(file: Path) =
|
||||
with(file) {
|
||||
if (showHiddenContents) {
|
||||
return !file.name.startsWith(".git")
|
||||
}
|
||||
if (isDirectory) {
|
||||
!isHidden
|
||||
if (isDirectory()) {
|
||||
!isHidden()
|
||||
} else {
|
||||
!isHidden && file.extension == "gpg"
|
||||
!isHidden() && file.extension == "gpg"
|
||||
}
|
||||
}
|
||||
|
||||
private fun listFiles(dir: File): Flow<File> {
|
||||
return dir.listFiles(::shouldTake)?.asFlow() ?: emptyFlow()
|
||||
private fun listFiles(dir: Path): Flow<Path> {
|
||||
return Files.newDirectoryStream(dir, ::shouldTake).asFlow()
|
||||
}
|
||||
|
||||
private fun listFilesRecursively(dir: File): Flow<File> {
|
||||
private fun listFilesRecursively(dir: Path): Flow<Path> {
|
||||
return dir
|
||||
.toFile()
|
||||
// Take top directory even if it is hidden.
|
||||
.walkTopDown()
|
||||
.onEnter { file -> file == dir || shouldTake(file) }
|
||||
.onEnter { file -> file.toPath() == dir || shouldTake(file.toPath()) }
|
||||
.asFlow()
|
||||
// Skip the root directory
|
||||
.drop(1)
|
||||
|
|
@ -296,24 +307,25 @@ constructor(
|
|||
yield()
|
||||
it
|
||||
}
|
||||
.map { it.toPath() }
|
||||
.filter(::shouldTake)
|
||||
}
|
||||
|
||||
private val _currentDir = MutableStateFlow(root)
|
||||
val currentDir = _currentDir.asStateFlow()
|
||||
|
||||
data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
|
||||
data class NavigationStackEntry(val dir: Path, val recyclerViewState: Parcelable?)
|
||||
|
||||
private val navigationStack = ArrayDeque<NavigationStackEntry>()
|
||||
|
||||
fun navigateTo(
|
||||
newDirectory: File = root,
|
||||
newDirectory: Path = root,
|
||||
listMode: ListMode = ListMode.AllEntries,
|
||||
recyclerViewState: Parcelable? = null,
|
||||
pushPreviousLocation: Boolean = true,
|
||||
) {
|
||||
if (!newDirectory.exists()) return
|
||||
require(newDirectory.isDirectory) { "Can only navigate to a directory" }
|
||||
require(newDirectory.isDirectory()) { "Can only navigate to a directory" }
|
||||
if (pushPreviousLocation) {
|
||||
navigationStack.addFirst(NavigationStackEntry(_currentDir.value, recyclerViewState))
|
||||
}
|
||||
|
|
@ -353,12 +365,12 @@ constructor(
|
|||
|
||||
fun search(
|
||||
filter: String,
|
||||
baseDirectory: File? = null,
|
||||
baseDirectory: Path? = null,
|
||||
filterMode: FilterMode = FilterMode.Fuzzy,
|
||||
searchMode: SearchMode? = null,
|
||||
listMode: ListMode = ListMode.AllEntries,
|
||||
) {
|
||||
require(baseDirectory?.isDirectory != false) { "Can only search in a directory" }
|
||||
require(baseDirectory?.isDirectory() != false) { "Can only search in a directory" }
|
||||
searchActionFlow.update {
|
||||
makeSearchAction(
|
||||
filter = filter,
|
||||
|
|
@ -401,7 +413,7 @@ constructor(
|
|||
private object PasswordItemDiffCallback : DiffUtil.ItemCallback<PasswordItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: PasswordItem, newItem: PasswordItem) =
|
||||
oldItem.file.absolutePath == newItem.file.absolutePath
|
||||
oldItem.file.absolutePathString() == newItem.file.absolutePathString()
|
||||
|
||||
override fun areContentsTheSame(oldItem: PasswordItem, newItem: PasswordItem) = oldItem == newItem
|
||||
}
|
||||
|
|
@ -479,11 +491,11 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
|
|||
fun requireSelectionTracker() = selectionTracker!!
|
||||
|
||||
private val selectedFiles
|
||||
get() = requireSelectionTracker().selection.map { File(it) }
|
||||
get() = requireSelectionTracker().selection.map { Paths.get(it) }
|
||||
|
||||
fun getSelectedItems() = selectedFiles.map { it.toPasswordItem() }
|
||||
|
||||
fun getPositionForFile(file: File) = itemKeyProvider.getPosition(file.absolutePath)
|
||||
fun getPositionForFile(file: Path) = itemKeyProvider.getPosition(file.absolutePathString())
|
||||
|
||||
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): T {
|
||||
val view = LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,17 @@ import app.passwordstore.crypto.errors.NoKeysAvailableException
|
|||
import app.passwordstore.crypto.errors.UnusableKeyException
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import com.github.michaelbull.result.unwrap
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.readBytes
|
||||
import kotlin.io.path.writeBytes
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
|
|
@ -34,7 +42,7 @@ public class PGPKeyManager
|
|||
constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
||||
KeyManager<PGPKey, PGPIdentifier> {
|
||||
|
||||
private val keyDir = File(filesDir, KEY_DIR_NAME)
|
||||
private val keyDir = Paths.get(filesDir, KEY_DIR_NAME)
|
||||
|
||||
/** @see KeyManager.addKey */
|
||||
override suspend fun addKey(key: PGPKey, replace: Boolean): Result<PGPKey, Throwable> =
|
||||
|
|
@ -43,7 +51,7 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
|||
if (!keyDirExists()) throw KeyDirectoryUnavailableException
|
||||
val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException
|
||||
if (!isKeyUsable(key)) throw UnusableKeyException
|
||||
val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION")
|
||||
val keyFile = keyDir.resolve("${tryGetId(key)}.$KEY_EXTENSION")
|
||||
if (keyFile.exists()) {
|
||||
val existingKeyBytes = keyFile.readBytes()
|
||||
val existingKeyRing =
|
||||
|
|
@ -65,7 +73,7 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
|||
throw KeyAlreadyExistsException(
|
||||
tryGetId(key)?.toString() ?: "Failed to retrieve key ID"
|
||||
)
|
||||
if (!keyFile.delete()) throw KeyDeletionFailedException
|
||||
if (!keyFile.deleteIfExists()) throw KeyDeletionFailedException
|
||||
}
|
||||
|
||||
keyFile.writeBytes(key.contents)
|
||||
|
|
@ -80,10 +88,8 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
|||
runSuspendCatching {
|
||||
if (!keyDirExists()) throw KeyDirectoryUnavailableException
|
||||
val key = getKeyById(identifier).unwrap()
|
||||
val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION")
|
||||
if (keyFile.exists()) {
|
||||
if (!keyFile.delete()) throw KeyDeletionFailedException
|
||||
}
|
||||
val keyFile = keyDir.resolve("${tryGetId(key)}.$KEY_EXTENSION")
|
||||
if (!keyFile.deleteIfExists()) throw KeyDeletionFailedException
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,8 +98,8 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
|||
withContext(dispatcher) {
|
||||
runSuspendCatching {
|
||||
if (!keyDirExists()) throw KeyDirectoryUnavailableException
|
||||
val keyFiles = keyDir.listFiles()
|
||||
if (keyFiles.isNullOrEmpty()) throw NoKeysAvailableException
|
||||
val keyFiles = keyDir.listDirectoryEntries().filter { it.isRegularFile() }
|
||||
if (keyFiles.isEmpty()) throw NoKeysAvailableException
|
||||
val keys = keyFiles.map { file -> PGPKey(file.readBytes()) }
|
||||
|
||||
val matchResult =
|
||||
|
|
@ -128,8 +134,8 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
|||
withContext(dispatcher) {
|
||||
runSuspendCatching {
|
||||
if (!keyDirExists()) throw KeyDirectoryUnavailableException
|
||||
val keyFiles = keyDir.listFiles()
|
||||
if (keyFiles.isNullOrEmpty()) return@runSuspendCatching emptyList()
|
||||
val keyFiles = keyDir.listDirectoryEntries().filter { it.isRegularFile() }
|
||||
if (keyFiles.isEmpty()) return@runSuspendCatching emptyList()
|
||||
keyFiles.map { keyFile -> PGPKey(keyFile.readBytes()) }.toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +145,7 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
|
|||
|
||||
/** Checks if [keyDir] exists and attempts to create it if not. */
|
||||
private fun keyDirExists(): Boolean {
|
||||
return keyDir.exists() || keyDir.mkdirs()
|
||||
return keyDir.exists() || runCatching { keyDir.createDirectories() }.isOk
|
||||
}
|
||||
|
||||
public companion object {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import app.passwordstore.crypto.errors.NoKeysAvailableException
|
|||
import app.passwordstore.crypto.errors.UnusableKeyException
|
||||
import com.github.michaelbull.result.unwrap
|
||||
import com.github.michaelbull.result.unwrapError
|
||||
import java.io.File
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.name
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
|
@ -26,9 +28,9 @@ class PGPKeyManagerTest {
|
|||
|
||||
@get:Rule val temporaryFolder: TemporaryFolder = TemporaryFolder()
|
||||
private val dispatcher = StandardTestDispatcher()
|
||||
private val filesDir by unsafeLazy { temporaryFolder.root }
|
||||
private val keysDir by unsafeLazy { File(filesDir, PGPKeyManager.KEY_DIR_NAME) }
|
||||
private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePath, dispatcher) }
|
||||
private val filesDir by unsafeLazy { temporaryFolder.root.toPath() }
|
||||
private val keysDir by unsafeLazy { filesDir.resolve(PGPKeyManager.KEY_DIR_NAME) }
|
||||
private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePathString(), dispatcher) }
|
||||
private val secretKey = PGPKey(TestUtils.getArmoredSecretKey())
|
||||
private val publicKey = PGPKey(TestUtils.getArmoredPublicKey())
|
||||
|
||||
|
|
@ -42,10 +44,10 @@ class PGPKeyManagerTest {
|
|||
val keyId = keyManager.getKeyId(keyManager.addKey(secretKey).unwrap())
|
||||
assertEquals(KeyId(CryptoConstants.KEY_ID), keyId)
|
||||
// Check if the keys directory have one file
|
||||
assertEquals(1, filesDir.list()?.size)
|
||||
assertEquals(1, filesDir.listDirectoryEntries().size)
|
||||
// Check if the file name is correct
|
||||
val keyFile = keysDir.listFiles()?.first()
|
||||
assertEquals(keyFile?.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}")
|
||||
val keyFile = keysDir.listDirectoryEntries().first()
|
||||
assertEquals(keyFile.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue