1
Fork 0
photoview/api/scanner/scanner_user.go

444 lines
12 KiB
Go
Raw Normal View History

2020-02-01 17:58:45 +01:00
package scanner
import (
"container/list"
"database/sql"
"fmt"
"io/ioutil"
"log"
"os"
"path"
2020-02-12 18:10:52 +01:00
"strconv"
"strings"
"time"
2020-02-01 17:58:45 +01:00
"github.com/viktorstrate/photoview/api/graphql/models"
2020-02-21 20:51:50 +01:00
"github.com/viktorstrate/photoview/api/graphql/notification"
"github.com/viktorstrate/photoview/api/utils"
2020-02-01 17:58:45 +01:00
)
2020-04-15 11:21:16 +02:00
func ScanAll(database *sql.DB) error {
rows, err := database.Query("SELECT * FROM user")
if err != nil {
log.Printf("Could not fetch all users from database: %s\n", err.Error())
return err
}
users, err := models.NewUsersFromRows(rows)
if err != nil {
log.Printf("Could not convert users: %s\n", err)
return err
}
for _, user := range users {
go scan(database, user)
}
return nil
}
2020-02-09 21:25:33 +01:00
func ScanUser(database *sql.DB, userId int) error {
2020-02-01 17:58:45 +01:00
row := database.QueryRow("SELECT * FROM user WHERE user_id = ?", userId)
user, err := models.NewUserFromRow(row)
if err != nil {
log.Printf("Could not find user to scan: %s\n", err.Error())
return err
}
log.Printf("Starting scan for user '%s'\n", user.Username)
go scan(database, user)
return nil
}
func scan(database *sql.DB, user *models.User) {
2020-02-21 20:51:50 +01:00
// Check if user directory exists on the file system
if _, err := os.Stat(user.RootPath); err != nil {
if os.IsNotExist(err) {
ScannerError("Photo directory for user '%s' does not exist '%s'\n", user.Username, user.RootPath)
} else {
ScannerError("Could not read photo directory for user '%s': %s\n", user.Username, user.RootPath)
}
return
}
2020-02-21 20:51:50 +01:00
notifyKey := utils.GenerateToken()
2020-02-26 19:44:47 +01:00
processKey := utils.GenerateToken()
notifyThrottle := utils.NewThrottle(500 * time.Millisecond)
2020-02-21 20:51:50 +01:00
timeout := 3000
2020-02-21 20:51:50 +01:00
notification.BroadcastNotification(&models.Notification{
Key: notifyKey,
Type: models.NotificationTypeMessage,
Header: "User scan started",
2020-04-15 11:27:34 +02:00
Content: fmt.Sprintf("Scanning has started for user '%s'", user.Username),
Timeout: &timeout,
2020-02-21 20:51:50 +01:00
})
2020-02-12 18:10:52 +01:00
// Start scanning
cache := MakeScannerCache()
2020-02-09 14:21:53 +01:00
2020-02-01 17:58:45 +01:00
type scanInfo struct {
path string
parentId *int
}
scanQueue := list.New()
scanQueue.PushBack(scanInfo{
path: user.RootPath,
parentId: nil,
})
newPhotos := make([]*models.Photo, 0)
2020-02-01 17:58:45 +01:00
for scanQueue.Front() != nil {
albumInfo := scanQueue.Front().Value.(scanInfo)
scanQueue.Remove(scanQueue.Front())
albumPath := albumInfo.path
albumParentId := albumInfo.parentId
cache.album_paths_scanned = append(cache.album_paths_scanned, albumPath)
2020-02-12 18:10:52 +01:00
2020-02-01 17:58:45 +01:00
// Read path
dirContent, err := ioutil.ReadDir(albumPath)
if err != nil {
2020-02-23 11:59:57 +01:00
ScannerError("Could not read directory: %s\n", err.Error())
continue
2020-02-01 17:58:45 +01:00
}
tx, err := database.Begin()
if err != nil {
2020-02-23 11:59:57 +01:00
ScannerError("Could not begin database transaction: %s\n", err)
continue
2020-02-01 17:58:45 +01:00
}
log.Printf("Scanning directory: %s", albumPath)
// Make album if not exists
albumTitle := path.Base(albumPath)
_, err = tx.Exec("INSERT IGNORE INTO album (title, parent_album, owner_id, path) VALUES (?, ?, ?, ?)", albumTitle, albumParentId, user.UserID, albumPath)
if err != nil {
2020-02-23 11:59:57 +01:00
ScannerError("Could not insert album into database: %s\n", err)
2020-02-01 17:58:45 +01:00
tx.Rollback()
2020-02-23 11:59:57 +01:00
continue
2020-02-01 17:58:45 +01:00
}
row := tx.QueryRow("SELECT * FROM album WHERE path = ?", albumPath)
album, err := models.NewAlbumFromRow(row)
if err != nil {
ScannerError("Could not get album: %s\n", err)
2020-02-01 17:58:45 +01:00
tx.Rollback()
return
}
2020-02-02 18:18:38 +01:00
// Commit album transaction
if err := tx.Commit(); err != nil {
log.Printf("ERROR: Could not commit database transaction: %s\n", err)
return
}
2020-02-01 17:58:45 +01:00
// Scan for photos
newFoundPhotos, err := findPhotosForAlbum(album, &cache, database, func(photo *models.Photo, newPhoto bool) {
notifyThrottle.Trigger(func() {
notification.BroadcastNotification(&models.Notification{
Key: processKey,
Type: models.NotificationTypeMessage,
Header: fmt.Sprintf("Scanning photo for user '%s'", user.Username),
Content: fmt.Sprintf("Scanning image at %s", photo.Path),
})
})
})
if err != nil {
ScannerError("Failed to scan album for new photos (album_id %d)", album.AlbumID)
2020-02-01 17:58:45 +01:00
}
newPhotos = append(newPhotos, newFoundPhotos...)
2020-02-01 17:58:45 +01:00
// Scan for sub-albums
for _, item := range dirContent {
subalbumPath := path.Join(albumPath, item.Name())
2020-03-07 15:34:32 +01:00
// Skip if directory is hidden
if path.Base(subalbumPath)[0:1] == "." {
continue
}
if item.IsDir() && directoryContainsPhotos(subalbumPath, &cache) {
2020-02-01 17:58:45 +01:00
scanQueue.PushBack(scanInfo{
path: subalbumPath,
parentId: &album.AlbumID,
2020-02-01 17:58:45 +01:00
})
}
}
}
completeMessage := "No new photos were found"
if len(newPhotos) > 0 {
completeMessage = fmt.Sprintf("%d new photos were found", len(newPhotos))
}
2020-02-21 20:51:50 +01:00
notification.BroadcastNotification(&models.Notification{
Key: notifyKey,
Type: models.NotificationTypeMessage,
2020-04-15 11:27:34 +02:00
Header: fmt.Sprintf("Scan completed for user '%s'", user.Username),
Content: completeMessage,
2020-02-21 20:51:50 +01:00
Positive: true,
})
cleanupCache(database, &cache, user)
2020-02-27 16:26:53 +01:00
err := processUnprocessedPhotos(database, user, notifyKey)
if err != nil {
log.Printf("ERROR: processing photos: %s\n", err)
}
2020-02-26 19:44:47 +01:00
2020-04-15 11:27:34 +02:00
log.Printf("Done scanning user '%s'\n", user.Username)
2020-02-01 17:58:45 +01:00
}
func directoryContainsPhotos(rootPath string, cache *ScannerCache) bool {
2020-02-12 18:45:58 +01:00
if contains_image := cache.album_contains_photo(rootPath); contains_image != nil {
return *contains_image
}
2020-02-01 17:58:45 +01:00
scanQueue := list.New()
scanQueue.PushBack(rootPath)
2020-02-12 18:45:58 +01:00
scanned_directories := make([]string, 0)
2020-02-01 17:58:45 +01:00
for scanQueue.Front() != nil {
dirPath := scanQueue.Front().Value.(string)
scanQueue.Remove(scanQueue.Front())
2020-02-12 18:45:58 +01:00
scanned_directories = append(scanned_directories, dirPath)
2020-02-01 17:58:45 +01:00
dirContent, err := ioutil.ReadDir(dirPath)
if err != nil {
2020-02-23 11:59:57 +01:00
ScannerError("Could not read directory: %s\n", err.Error())
2020-02-01 17:58:45 +01:00
return false
}
for _, fileInfo := range dirContent {
filePath := path.Join(dirPath, fileInfo.Name())
if fileInfo.IsDir() {
scanQueue.PushBack(filePath)
} else {
2020-02-09 14:21:53 +01:00
if isPathImage(filePath, cache) {
2020-02-12 18:45:58 +01:00
cache.insert_album_paths(dirPath, rootPath, true)
2020-02-01 17:58:45 +01:00
return true
}
}
}
}
2020-02-12 18:45:58 +01:00
for _, scanned_path := range scanned_directories {
cache.insert_album_path(scanned_path, false)
}
2020-02-01 17:58:45 +01:00
return false
}
2020-02-12 18:10:52 +01:00
2020-02-27 16:26:53 +01:00
func processUnprocessedPhotos(database *sql.DB, user *models.User, notifyKey string) error {
processKey := utils.GenerateToken()
notifyThrottle := utils.NewThrottle(500 * time.Millisecond)
2020-02-27 16:26:53 +01:00
rows, err := database.Query(`
SELECT photo.* FROM photo JOIN album ON photo.album_id = album.album_id
WHERE album.owner_id = ?
AND photo.photo_id NOT IN (
SELECT photo_id FROM photo_url WHERE photo_url.photo_id = photo.photo_id
)
`, user.UserID)
if err != nil {
ScannerError("Could not get photos to process from db")
return err
}
photosToProcess, err := models.NewPhotosFromRows(rows)
if err != nil {
if err == sql.ErrNoRows {
// No photos to process
return nil
}
ScannerError("Could not parse photos to process from db %s", err)
return err
}
// Proccess all photos
for count, photo := range photosToProcess {
tx, err := database.Begin()
if err != nil {
ScannerError("Could not start database transaction: %s", err)
continue
}
notifyThrottle.Trigger(func() {
var progress float64 = float64(count) / float64(len(photosToProcess)) * 100.0
2020-02-27 16:26:53 +01:00
notification.BroadcastNotification(&models.Notification{
Key: processKey,
Type: models.NotificationTypeProgress,
2020-04-15 11:27:34 +02:00
Header: fmt.Sprintf("Processing photos (%d of %d) for user '%s'", count, len(photosToProcess), user.Username),
Content: fmt.Sprintf("Processing photo at %s", photo.Path),
Progress: &progress,
})
2020-02-27 16:26:53 +01:00
})
err = ProcessPhoto(tx, photo)
if err != nil {
tx.Rollback()
ScannerError("Could not process photo (%s): %s", photo.Path, err)
2020-02-27 16:26:53 +01:00
continue
}
err = tx.Commit()
if err != nil {
ScannerError("Could not commit db transaction: %s", err)
continue
}
}
if len(photosToProcess) > 0 {
notification.BroadcastNotification(&models.Notification{
Key: notifyKey,
Type: models.NotificationTypeMessage,
2020-04-15 11:27:34 +02:00
Header: fmt.Sprintf("Processing photos for user '%s' has completed", user.Username),
2020-02-27 16:26:53 +01:00
Content: fmt.Sprintf("%d photos have been processed", len(photosToProcess)),
Positive: true,
})
notification.BroadcastNotification(&models.Notification{
Key: processKey,
Type: models.NotificationTypeClose,
})
}
return nil
}
func cleanupCache(database *sql.DB, cache *ScannerCache, user *models.User) {
if len(cache.album_paths_scanned) == 0 {
2020-02-12 18:10:52 +01:00
return
}
// Delete old albums
album_args := make([]interface{}, 0)
album_args = append(album_args, user.UserID)
album_args = append(album_args, cache.album_paths_scanned...)
albums_questions := strings.Repeat("?,", len(cache.album_paths_scanned))[:len(cache.album_paths_scanned)*2-1]
rows, err := database.Query("SELECT album_id FROM album WHERE album.owner_id = ? AND path NOT IN ("+albums_questions+")", album_args...)
2020-02-12 18:10:52 +01:00
if err != nil {
2020-02-23 11:59:57 +01:00
ScannerError("Could not get albums from database: %s\n", err)
2020-02-12 18:10:52 +01:00
return
}
defer rows.Close()
2020-02-12 18:10:52 +01:00
deleted_album_ids := make([]interface{}, 0)
2020-02-12 18:10:52 +01:00
for rows.Next() {
var album_id int
if err := rows.Scan(&album_id); err != nil {
ScannerError("Could not parse album to be removed (album_id %d): %s\n", album_id, err)
}
deleted_album_ids = append(deleted_album_ids, album_id)
cache_path := path.Join("./photo_cache", strconv.Itoa(album_id))
2020-02-12 18:10:52 +01:00
err := os.RemoveAll(cache_path)
if err != nil {
2020-02-23 11:59:57 +01:00
ScannerError("Could not delete unused cache folder: %s\n%s\n", cache_path, err)
2020-02-12 18:10:52 +01:00
}
}
if len(deleted_album_ids) > 0 {
albums_questions = strings.Repeat("?,", len(deleted_album_ids))[:len(deleted_album_ids)*2-1]
2020-02-26 19:44:47 +01:00
if _, err := database.Exec("DELETE FROM album WHERE album_id IN ("+albums_questions+")", deleted_album_ids...); err != nil {
2020-02-26 19:44:47 +01:00
ScannerError("Could not delete old albums from database:\n%s\n", err)
2020-02-12 18:10:52 +01:00
}
}
// Delete old photos
photo_args := make([]interface{}, 0)
photo_args = append(photo_args, user.UserID)
photo_args = append(photo_args, cache.photo_paths_scanned...)
photo_questions := strings.Repeat("?,", len(cache.photo_paths_scanned))[:len(cache.photo_paths_scanned)*2-1]
rows, err = database.Query(`
SELECT photo.photo_id as photo_id, album.album_id as album_id FROM photo JOIN album ON photo.album_id = album.album_id
WHERE album.owner_id = ? AND photo.path NOT IN (`+photo_questions+`)
`, photo_args...)
if err != nil {
ScannerError("Could not get deleted photos from database: %s\n", err)
return
}
defer rows.Close()
deleted_photo_ids := make([]interface{}, 0)
for rows.Next() {
var photo_id int
var album_id int
if err := rows.Scan(&photo_id, &album_id); err != nil {
ScannerError("Could not parse photo to be removed (album_id %d, photo_id %d): %s\n", album_id, photo_id, err)
}
deleted_photo_ids = append(deleted_photo_ids, photo_id)
cache_path := path.Join("./photo_cache", strconv.Itoa(album_id), strconv.Itoa(photo_id))
err := os.RemoveAll(cache_path)
if err != nil {
ScannerError("Could not delete unused cache photo folder: %s\n%s\n", cache_path, err)
}
}
if len(deleted_photo_ids) > 0 {
photo_questions = strings.Repeat("?,", len(deleted_photo_ids))[:len(deleted_photo_ids)*2-1]
if _, err := database.Exec("DELETE FROM photo WHERE photo_id IN ("+photo_questions+")", deleted_photo_ids...); err != nil {
ScannerError("Could not delete old photos from database:\n%s\n", err)
}
}
2020-02-26 19:44:47 +01:00
if len(deleted_album_ids) > 0 || len(deleted_photo_ids) > 0 {
timeout := 3000
2020-02-26 19:44:47 +01:00
notification.BroadcastNotification(&models.Notification{
Key: utils.GenerateToken(),
Type: models.NotificationTypeMessage,
Header: "Deleted old photos",
Content: fmt.Sprintf("Deleted %d albums and %d photos, that was not found on disk", len(deleted_album_ids), len(deleted_photo_ids)),
Timeout: &timeout,
2020-02-26 19:44:47 +01:00
})
2020-02-12 18:10:52 +01:00
}
}
2020-02-23 11:59:57 +01:00
func ScannerError(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
log.Printf("ERROR: %s", message)
notification.BroadcastNotification(&models.Notification{
Key: utils.GenerateToken(),
Type: models.NotificationTypeMessage,
Header: "Scanner error",
Content: message,
Negative: true,
})
}
2020-02-23 12:43:45 +01:00
func PhotoCache() string {
photoCache := os.Getenv("PHOTO_CACHE")
if photoCache == "" {
photoCache = "./photo_cache"
}
return photoCache
}