Backend: Improve inline code comments

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-09-30 20:41:36 +02:00
parent 52cc66af6d
commit e3925d779f
13 changed files with 53 additions and 43 deletions

View File

@@ -66,7 +66,7 @@ import (
var initThumbsMutex sync.Mutex
// Config holds database, cache and all parameters of photoprism
// Config aggregates CLI flags, options.yml overrides, runtime settings, and shared resources (database, caches) for the running instance.
type Config struct {
once sync.Once
cliCtx *cli.Context
@@ -140,7 +140,7 @@ func initLogger() {
})
}
// NewConfig initialises a new configuration file
// NewConfig builds a Config from CLI context defaults and loads options.yml overrides if present.
func NewConfig(ctx *cli.Context) *Config {
start := false
@@ -172,7 +172,7 @@ func NewConfig(ctx *cli.Context) *Config {
return c
}
// Init creates directories, parses additional config files, opens a database connection and initializes dependencies.
// Init creates directories, parses additional config files, opens the database connection, and initializes dependent subsystems.
func (c *Config) Init() error {
start := time.Now()

View File

@@ -159,7 +159,7 @@ func (m File) RegenerateIndex() {
log.Debugf("search: updated %s [%s]", scope, time.Since(start))
}
// FirstFileByHash gets a file in db from its hash
// FirstFileByHash fetches the first file with the given hash, including soft-deleted rows via UnscopedDb.
func FirstFileByHash(fileHash string) (File, error) {
var file File
@@ -168,7 +168,7 @@ func FirstFileByHash(fileHash string) (File, error) {
return file, res.Error
}
// PrimaryFile returns the primary file for a photo uid.
// PrimaryFile finds the photo's primary file (primary flag set) regardless of soft-delete state.
func PrimaryFile(photoUid string) (*File, error) {
file := File{}

View File

@@ -202,7 +202,7 @@ func (m *Label) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
}
// FirstOrCreateLabel returns the existing label, inserts a new label or nil in case of errors.
// FirstOrCreateLabel reuses an existing label matched by slug/custom slug or creates and returns a new one; nil signals lookup/create failure.
func FirstOrCreateLabel(m *Label) *Label {
if m.LabelSlug == "" && m.CustomSlug == "" {
return nil

View File

@@ -94,7 +94,7 @@ func CachePhotoLabels() (err error) {
return nil
}
// FindLabel find the matching label based on the name provided or an error if not found.
// FindLabel resolves a label by name/slug, optionally consulting the in-memory cache before querying the database.
func FindLabel(name string, cached bool) (*Label, error) {
if name == "" {
return &Label{}, errors.New("missing label name")
@@ -139,7 +139,7 @@ func FindLabel(name string, cached bool) (*Label, error) {
return result, nil
}
// FindPhotoLabel find a photo label assignment for the specified IDs.
// FindPhotoLabel loads the photo-label join row for the given IDs, using the cache when enabled.
func FindPhotoLabel(photoId, labelId uint, cached bool) (*PhotoLabel, error) {
if photoId == 0 {
return &PhotoLabel{}, errors.New("invalid photo id")

View File

@@ -34,7 +34,7 @@ var MetadataEstimateInterval = 24 * 7 * time.Hour // 7 Days
var photoMutex = sync.Mutex{}
// MapKey returns a key referencing time and location for indexing.
// MapKey builds a deterministic indexing key from the capture timestamp and spatial cell identifier.
func MapKey(takenAt time.Time, cellId string) string {
return path.Join(strconv.FormatInt(takenAt.Unix(), 36), cellId)
}
@@ -112,12 +112,12 @@ func (Photo) TableName() string {
return "photos"
}
// NewPhoto creates a new photo with default values.
// NewPhoto returns a Photo with default metadata placeholders and the requested stack flag.
func NewPhoto(stackable bool) Photo {
return NewUserPhoto(stackable, "")
}
// NewUserPhoto creates a photo owned by a user.
// NewUserPhoto returns a Photo initialized for the given user UID, including default Unknown* references and stack state.
func NewUserPhoto(stackable bool, userUid string) Photo {
m := Photo{
PhotoTitle: UnknownTitle,
@@ -144,7 +144,8 @@ func NewUserPhoto(stackable bool, userUid string) Photo {
return m
}
// SavePhotoForm saves a model in the database using form data.
// SavePhotoForm merges a photo form submission into the Photo, normalizes data, refreshes derived metadata, and persists the changes.
// The photo must already exist in the database; after saving, derived counters are updated asynchronously.
func SavePhotoForm(m *Photo, form form.Photo) error {
if m == nil {
return fmt.Errorf("photo is nil")
@@ -328,7 +329,7 @@ func (m *Photo) String() string {
return "*Photo"
}
// FirstOrCreate fetches an existing row from the database or inserts a new one.
// FirstOrCreate inserts the Photo if it does not exist and otherwise reloads the persisted row with its associations.
func (m *Photo) FirstOrCreate() *Photo {
if err := m.Create(); err == nil {
return m
@@ -339,7 +340,7 @@ func (m *Photo) FirstOrCreate() *Photo {
return FindPhoto(*m)
}
// Create inserts a new photo to the database.
// Create persists a new Photo while holding the package mutex and ensures the related Details record exists.
func (m *Photo) Create() error {
photoMutex.Lock()
defer photoMutex.Unlock()
@@ -355,7 +356,7 @@ func (m *Photo) Create() error {
return nil
}
// Save updates the record in the database or inserts a new record if it does not already exist.
// Save writes Photo changes, creates missing rows, and re-resolves the primary file relationship.
func (m *Photo) Save() error {
photoMutex.Lock()
defer photoMutex.Unlock()
@@ -371,7 +372,7 @@ func (m *Photo) Save() error {
return m.ResolvePrimary()
}
// FindPhoto fetches the matching record or returns null if it was not found.
// FindPhoto looks up a Photo by UID or numeric ID and preloads key associations used by higher layers.
func FindPhoto(find Photo) *Photo {
if find.PhotoUID == "" && find.ID == 0 {
return nil
@@ -414,7 +415,7 @@ func (m *Photo) Find() *Photo {
return FindPhoto(*m)
}
// SaveLabels updates the photo after labels have changed.
// SaveLabels recalculates derived metadata after label edits, persists the Photo, and schedules count updates.
func (m *Photo) SaveLabels() error {
if !m.HasID() {
return errors.New("photo: cannot save to database, id is empty")
@@ -450,7 +451,7 @@ func (m *Photo) SaveLabels() error {
return nil
}
// ClassifyLabels returns all associated labels as classify.Labels
// ClassifyLabels converts attached PhotoLabel relations into classify.Labels for downstream AI components.
func (m *Photo) ClassifyLabels() classify.Labels {
result := classify.Labels{}
@@ -516,7 +517,7 @@ func (m *Photo) RemoveKeyword(w string) error {
return nil
}
// UpdateLabels updates labels that are automatically set based on the photo title, subject, and keywords.
// UpdateLabels refreshes automatically generated labels derived from the title, caption, subject metadata, and keywords.
func (m *Photo) UpdateLabels() error {
if err := m.UpdateTitleLabels(); err != nil {
return err
@@ -609,7 +610,7 @@ func (m *Photo) UpdateKeywordLabels() error {
return Db().Where("label_src = ? AND photo_id = ? AND label_id NOT IN (?)", classify.SrcKeyword, m.ID, labelIds).Delete(&PhotoLabel{}).Error
}
// IndexKeywords adds given keywords to the photo entry
// IndexKeywords synchronizes the photo-keyword join table based on normalized keywords from titles, captions, and metadata.
func (m *Photo) IndexKeywords() error {
db := UnscopedDb()
details := m.GetDetails()
@@ -721,7 +722,7 @@ func (m *Photo) UnknownLens() bool {
return m.LensID == 0 || m.LensID == UnknownLens.ID
}
// GetDetails returns optional photo metadata.
// GetDetails loads or lazily creates the Details record backing optional photo metadata.
func (m *Photo) GetDetails() *Details {
if m.Details != nil {
m.Details.PhotoID = m.ID
@@ -778,7 +779,7 @@ func (m *Photo) ShouldGenerateLabels(force bool) bool {
return true
}
// AddLabels updates the entity with additional or updated label information.
// AddLabels ensures classify labels exist as Label entities and attaches them to the photo, tightening uncertainty when higher confidence values arrive.
func (m *Photo) AddLabels(labels classify.Labels) {
for _, classifyLabel := range labels {
labelEntity := FirstOrCreateLabel(NewLabel(classifyLabel.Title(), classifyLabel.Priority))

View File

@@ -25,7 +25,7 @@ func NewFiles() *Files {
return m
}
// Init fetches the list from the database once.
// Init lazily loads the indexed file map from the database and stores the initial count.
func (m *Files) Init() error {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -50,7 +50,7 @@ func (m *Files) Init() error {
}
}
// Done should be called after all files have been processed.
// Done clears the in-memory cache so the next index pass reloads a fresh snapshot.
func (m *Files) Done() {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -62,7 +62,7 @@ func (m *Files) Done() {
m.files = make(query.FileMap)
}
// Remove a file from the lookup table.
// Remove evicts a file entry from the cache (e.g. after deletion or re-import).
func (m *Files) Remove(fileName, fileRoot string) {
key := path.Join(fileRoot, fileName)
@@ -72,7 +72,7 @@ func (m *Files) Remove(fileName, fileRoot string) {
delete(m.files, key)
}
// Ignore tests of a file requires indexing, file name must be relative to the originals path.
// Ignore determines whether a file should be skipped based on modification timestamp, updating the cache when needed.
func (m *Files) Ignore(fileName, fileRoot string, modTime time.Time, rescan bool) bool {
timestamp := modTime.UTC().Truncate(time.Second).Unix()
key := path.Join(fileRoot, fileName)
@@ -95,7 +95,7 @@ func (m *Files) Ignore(fileName, fileRoot string, modTime time.Time, rescan bool
}
}
// Indexed tests of a file was already indexed without modifying the files map.
// Indexed checks if a file has already been indexed without mutating the cache.
func (m *Files) Indexed(fileName, fileRoot string, modTime time.Time, rescan bool) bool {
if rescan {
return false
@@ -116,7 +116,7 @@ func (m *Files) Indexed(fileName, fileRoot string, modTime time.Time, rescan boo
}
}
// Exists tests of a file exists.
// Exists reports whether the given file key is present in the cache.
func (m *Files) Exists(fileName, fileRoot string) bool {
key := path.Join(fileRoot, fileName)

View File

@@ -12,6 +12,7 @@ func initIndex() {
services.Index = photoprism.NewIndex(Config(), Convert(), Files(), Photos())
}
// Index returns the singleton indexing service, initializing it on first use.
func Index() *photoprism.Index {
onceIndex.Do(initIndex)

View File

@@ -22,7 +22,7 @@ import (
"github.com/photoprism/photoprism/pkg/media"
)
// Index represents an indexer that indexes files in the originals directory.
// Index coordinates filesystem scans, metadata extraction, and database updates for originals.
type Index struct {
conf *config.Config
convert *Convert
@@ -67,7 +67,9 @@ func (ind *Index) Cancel() {
mutex.IndexWorker.Cancel()
}
// Start indexes media files in the "originals" folder.
// Start indexes media files in the originals folder according to the provided options.
// It streams work to worker goroutines, updates duplicate caches, and returns both
// the set of processed paths and the number of files that were changed.
func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
defer func() {
if r := recover(); r != nil {

View File

@@ -23,12 +23,13 @@ import (
"github.com/photoprism/photoprism/pkg/txt/clip"
)
// MediaFile indexes a single media file.
// MediaFile indexes a single media file on behalf of the default owner.
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID string) (result IndexResult) {
return ind.UserMediaFile(m, o, originalName, photoUID, entity.OwnerUnknown)
}
// UserMediaFile indexes a single media file owned by a user.
// UserMediaFile indexes a single media file for the provided owner, performing duplicate detection,
// metadata extraction, and database updates before returning an IndexResult describing the outcome.
func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, photoUID, userUID string) (result IndexResult) {
if m == nil {
result.Status = IndexFailed

View File

@@ -1,5 +1,6 @@
package photoprism
// IndexJob bundles the indexing context for a single media file and its related companions.
type IndexJob struct {
FileName string
Related RelatedFiles
@@ -7,6 +8,8 @@ type IndexJob struct {
Ind *Index
}
// IndexWorker consumes IndexJob messages and indexes the related files serially.
// It is intentionally lightweight so the caller can fan out multiple goroutines.
func IndexWorker(jobs <-chan IndexJob) {
for job := range jobs {
IndexRelated(job.Related, job.Ind, job.IndexOpt)

View File

@@ -10,14 +10,14 @@ import (
"github.com/photoprism/photoprism/internal/entity/query"
)
// Photos represents photo id lookup table, sorted by date and S2 cell id.
// Photos caches a lookup table from capture timestamp + S2 cell IDs to photo IDs so workers can skip redundant database scans.
type Photos struct {
count int
photos query.PhotoMap
mutex sync.RWMutex
}
// NewPhotos returns a new Photos instance.
// NewPhotos constructs an empty Photos cache. Call Init before using Find.
func NewPhotos() *Photos {
m := &Photos{
photos: make(query.PhotoMap),
@@ -26,7 +26,8 @@ func NewPhotos() *Photos {
return m
}
// Init fetches the list from the database once.
// Init hydrates the cache from the database if it has not been loaded yet.
// Subsequent calls are no-ops unless the internal map was reset.
func (m *Photos) Init() error {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -47,7 +48,7 @@ func (m *Photos) Init() error {
}
}
// Remove a photo from the lookup table.
// Remove evicts a photo from the lookup table when media has been deleted or re-indexed.
func (m *Photos) Remove(takenAt time.Time, cellId string) {
key := entity.MapKey(takenAt, cellId)
@@ -57,7 +58,7 @@ func (m *Photos) Remove(takenAt time.Time, cellId string) {
delete(m.photos, key)
}
// Find returns the photo ID for a time and cell id.
// Find returns the cached photo ID for the given capture time and cell. Zero means no entry exists.
func (m *Photos) Find(takenAt time.Time, cellId string) uint {
key := entity.MapKey(takenAt, cellId)

View File

@@ -33,7 +33,7 @@ const (
StatusCommunity Status = "ce"
)
// Config represents backend api credentials for maps & geodata.
// Config stores the encrypted Hub session and surface credentials used for maps and geodata requests.
type Config struct {
Version string `json:"version" yaml:"-"`
FileName string `json:"-" yaml:"-"`
@@ -49,7 +49,7 @@ type Config struct {
PartnerID string `json:"-" yaml:"-"`
}
// NewConfig creates a new backend api credentials instance.
// NewConfig constructs a Hub configuration with the supplied version metadata and defaults for dynamic values.
func NewConfig(version, fileName, serial, env, userAgent, partnerId string) *Config {
return &Config{
Version: version,
@@ -65,7 +65,7 @@ func NewConfig(version, fileName, serial, env, userAgent, partnerId string) *Con
}
}
// MapKey returns the maps api key.
// MapKey returns the decrypted maps API key by decoding the cached session when available.
func (c *Config) MapKey() string {
if sess, err := c.DecodeSession(true); err != nil {
return ""
@@ -74,7 +74,7 @@ func (c *Config) MapKey() string {
}
}
// Tier returns the membership tier.
// Tier returns the numeric membership tier from the decoded session; zero indicates none.
func (c *Config) Tier() int {
if sess, err := c.DecodeSession(true); err != nil {
return 0
@@ -101,7 +101,7 @@ func (c *Config) Customer() string {
}
}
// Propagate updates backend api credentials in other packages.
// Propagate publishes the current credentials to dependent packages (e.g. places search).
func (c *Config) Propagate() {
places.Key = c.Key
places.Secret = c.Secret

View File

@@ -84,7 +84,8 @@ func (w *Vision) originalsPath() string {
// Start runs the requested vision models against photos matching the search
// filter. `customSrc` allows the caller to override the metadata source string,
// `force` regenerates metadata regardless of existing values, and `runType`
// describes the scheduling context (manual, scheduled, etc.).
// describes the scheduling context (manual, scheduled, etc.). A global worker
// mutex prevents multiple vision jobs from running concurrently.
func (w *Vision) Start(filter string, count int, models []string, customSrc string, force bool, runType vision.RunType) (err error) {
defer func() {
if r := recover(); r != nil {