fix(library): default library name is now locale-aware, not the OS user name
defaultLibrary() no longer falls back to System.getProperty("user.name")
-- AppDirectories.dataDir() is already namespaced per OS user profile on
every platform, so that dedup never actually protected anything. It now
reads a friendly name ("My Photos" / "Mes photos") straight off the
messages* bundles for Locale.getDefault(), since no Spring context (and
so no I18nService) exists yet at this point in the bootstrap.
sanitize() now leaves a space untouched (safe in both a filename and the
JDBC URL) so "My Photos" survives as a real space instead of turning
into "My_Photos".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@ import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -31,9 +34,18 @@ public final class LibraryCatalog {
|
||||
static final String DATABASE_SUFFIX = ".mv.db";
|
||||
|
||||
/**
|
||||
* Used when {@code user.name} is unusable, so a library always has a name.
|
||||
* Used when a name is blank, and as {@link #defaultLibrary()}'s own ultimate fallback when no
|
||||
* translation is available for the current locale.
|
||||
*/
|
||||
static final String FALLBACK_NAME = "default";
|
||||
static final String FALLBACK_NAME = "My Photos";
|
||||
|
||||
/**
|
||||
* Message key for {@link #defaultLibrary()}'s own name, read straight from the {@code messages*
|
||||
* .properties} bundles Spring's {@code MessageSource} is itself built from — {@link #defaultLibrary()}
|
||||
* runs before there is a Spring context (see its own javadoc), so it cannot go through
|
||||
* {@code I18nService}, only around it.
|
||||
*/
|
||||
private static final String DEFAULT_NAME_KEY = "library.defaultName";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LibraryCatalog.class);
|
||||
|
||||
@@ -76,10 +88,12 @@ public final class LibraryCatalog {
|
||||
/**
|
||||
* Reduces {@code name} to {@code [A-Za-z0-9._-]}, replacing everything else with {@code _}.
|
||||
*
|
||||
* <p>Not cosmetic. The default library name comes from {@code user.name}, which the OS does not
|
||||
* constrain: a {@code /} would let the database file escape the data directory, and a {@code ;} would
|
||||
* append parameters to the JDBC URL the name is interpolated into. Leading dots are stripped too, so a
|
||||
* name can never produce a hidden file or a {@code ..} traversal.
|
||||
* <p>Not cosmetic. A user-supplied library name is not otherwise constrained: a {@code /} would let the
|
||||
* database file escape the data directory, and a {@code ;} would append parameters to the JDBC URL the
|
||||
* name is interpolated into. Leading dots are stripped too, so a name can never produce a hidden file or
|
||||
* a {@code ..} traversal. A space is left untouched — safe on every supported OS's filesystem and in the
|
||||
* JDBC URL alike — so {@link #defaultLibrary()}'s own "My Photos" survives as a real space rather than
|
||||
* turning into "My_Photos".
|
||||
*/
|
||||
public static String sanitize(@Nullable String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
@@ -98,11 +112,26 @@ public final class LibraryCatalog {
|
||||
}
|
||||
|
||||
/**
|
||||
* The library opened when nothing has been chosen yet: the OS user name, so two accounts on one
|
||||
* machine do not share an index.
|
||||
* The library opened when nothing has been chosen yet: a friendly, locale-appropriate name — not the
|
||||
* OS user name this used to be. {@link AppDirectories#dataDir()} is already namespaced per OS user
|
||||
* profile (macOS/Windows/Linux each resolve it under that user's own home or app-data directory), so a
|
||||
* fixed name here never actually risked two accounts on one machine sharing an index.
|
||||
*
|
||||
* <p>Resolved directly from the {@code messages} resource bundles against {@link Locale#getDefault()}
|
||||
* — the OS/JVM locale, since no {@code I18nService} (and so no user-chosen UI language) exists this
|
||||
* early — falling back to {@link #FALLBACK_NAME} if the bundle or key is ever missing.
|
||||
*/
|
||||
public static String defaultLibrary() {
|
||||
return sanitize(System.getProperty("user.name", FALLBACK_NAME));
|
||||
return sanitize(localizedDefaultName());
|
||||
}
|
||||
|
||||
private static String localizedDefaultName() {
|
||||
try {
|
||||
return ResourceBundle.getBundle("messages", Locale.getDefault()).getString(DEFAULT_NAME_KEY);
|
||||
}
|
||||
catch (MissingResourceException e) {
|
||||
return FALLBACK_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,6 +167,6 @@ public final class LibraryCatalog {
|
||||
return (c >= 'a' && c <= 'z')
|
||||
|| (c >= 'A' && c <= 'Z')
|
||||
|| (c >= '0' && c <= '9')
|
||||
|| c == '.' || c == '_' || c == '-';
|
||||
|| c == '.' || c == '_' || c == '-' || c == ' ';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ nav.maintenance.recognition=People & Pets
|
||||
nav.drawer.empty=Nothing here yet
|
||||
header.import.tooltip=Import from a card or external drive (coming soon)
|
||||
# --- Library folders (Photothèque drawer) ---
|
||||
# Read directly off the bundle by LibraryCatalog.defaultLibrary(), not through I18nService — see its own
|
||||
# javadoc for why (no Spring context exists yet at that point).
|
||||
library.defaultName=My Photos
|
||||
library.addFolder.tooltip=Monitor an existing folder.
|
||||
library.chooseFolder.dialogTitle=Choose a folder to add
|
||||
library.removeFolder.tooltip=Remove this folder
|
||||
|
||||
@@ -22,6 +22,9 @@ nav.maintenance.recognition=Personnes et animaux
|
||||
nav.drawer.empty=Rien ici pour l'instant
|
||||
header.import.tooltip=Importer depuis une carte ou un disque externe (bientôt disponible)
|
||||
# --- Dossiers de la photothèque (tiroir) ---
|
||||
# Lu directement dans le fichier de traductions par LibraryCatalog.defaultLibrary(), sans passer par
|
||||
# I18nService — voir sa propre javadoc (aucun contexte Spring n'existe encore à ce stade).
|
||||
library.defaultName=Mes photos
|
||||
library.addFolder.tooltip=Surveiller un dossier existant.
|
||||
library.chooseFolder.dialogTitle=Choisir un dossier à ajouter
|
||||
library.removeFolder.tooltip=Retirer ce dossier
|
||||
|
||||
@@ -59,9 +59,9 @@ class LibraryCatalogTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* The default name comes from {@code user.name}, which the OS does not constrain. A separator would let
|
||||
* the database file escape the data directory and a semicolon would append parameters to the JDBC URL
|
||||
* the name is interpolated into.
|
||||
* A user-supplied library name is not otherwise constrained. A separator would let the database file
|
||||
* escape the data directory and a semicolon would append parameters to the JDBC URL the name is
|
||||
* interpolated into.
|
||||
*/
|
||||
@Test
|
||||
void sanitizesNamesThatWouldEscapeTheDirectoryOrTheUrl() {
|
||||
@@ -78,6 +78,9 @@ class LibraryCatalogTest {
|
||||
softly.assertThat(LibraryCatalog.sanitize(" ")).isEqualTo(LibraryCatalog.FALLBACK_NAME);
|
||||
softly.assertThat(LibraryCatalog.sanitize(null)).isEqualTo(LibraryCatalog.FALLBACK_NAME);
|
||||
softly.assertThat(LibraryCatalog.sanitize("my-photos_2024.v2")).isEqualTo("my-photos_2024.v2");
|
||||
// A space is safe on every supported OS's filesystem and in the JDBC URL alike, so it survives
|
||||
// untouched — this is what keeps defaultLibrary()'s own "My Photos" a real space, not "My_Photos".
|
||||
softly.assertThat(LibraryCatalog.sanitize("My Photos")).isEqualTo("My Photos");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user