Compare commits

...

26 Commits

Author SHA1 Message Date
0255031150 Fix SC2002 2022-11-19 00:14:24 +01:00
55388fea39 Add listbyfile option 2022-11-19 00:08:54 +01:00
27f6db5410 Replace realpath with readlink -f 2022-11-19 00:08:23 +01:00
1458620bee Rename function 2022-11-19 00:07:17 +01:00
2dbe0b08c4 Add tag -i / --interactive 2022-11-19 00:06:14 +01:00
d95aa517df Improve docs 2022-11-19 00:01:22 +01:00
66a7c0e3e7 Check for existence of database at the start 2022-11-18 23:58:02 +01:00
69951be543 Pass on --db to the autoimport command 2022-11-18 14:05:31 +01:00
b483c72df7 Handle ~ in --db argument 2022-11-18 14:05:06 +01:00
e00766cce8 Properly escape everywhere 2022-11-16 12:43:06 +01:00
76b490bc83 Add more tests 2022-11-16 12:16:28 +01:00
d77349ce50 Fix for upstream bug 2022-11-16 11:54:52 +01:00
741f349028 Add another test 2022-11-16 11:53:25 +01:00
c3acf30c66 Fix adding hashes, add file_by_filename 2022-11-16 11:44:25 +01:00
1580865b56 Fix file path not being store properly 2022-11-16 11:01:28 +01:00
d162919b3d Add TODOs 2022-11-16 10:55:16 +01:00
90b0c98675 Add metadata table 2022-11-16 10:55:08 +01:00
41362eccf1 Split files into filenames and paths 2022-11-16 10:53:07 +01:00
f02133d6ab Allow more than one tag, fix listing tags 2022-11-16 09:59:44 +01:00
f43c1d433a Check if files exist before doing stuff 2022-11-16 09:39:47 +01:00
d7f4a9c299 Check if tags exist when tagging 2022-11-16 09:35:49 +01:00
473d88b3c9 Display usage when no arguments supplied 2022-11-16 09:02:52 +01:00
c86f125ed3 Add support for adding hashes 2022-11-16 08:56:58 +01:00
ca5146e17c Capitalize SQL column types 2022-11-16 08:56:33 +01:00
ec5cb5bb17 Add hash test 2022-11-16 08:56:15 +01:00
907f9b742d Fix wrong usage of $PWD 2022-11-16 08:08:13 +01:00
3 changed files with 247 additions and 55 deletions

View File

@ -1,26 +1,32 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "tags" (
"id" INTEGER,
"label" text,
"label" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "files" (
"id" INTEGER,
"filename" text,
"filename" TEXT NOT NULL,
"path" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "tags_ties" (
"id" INTEGER,
"tid" INTEGER,
"fid" INTEGER,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("tid") REFERENCES "tags"("id"),
CREATE TABLE IF NOT EXISTS "metadata" (
"fid" INTEGER NOT NULL,
"name" TEXT,
PRIMARY KEY("fid"),
FOREIGN KEY("fid") REFERENCES "files"("id")
);
CREATE TABLE IF NOT EXISTS "tags_ties" (
"fid" INTEGER NOT NULL,
"tid" INTEGER NOT NULL,
PRIMARY KEY("fid", "tid"),
FOREIGN KEY("fid") REFERENCES "files"("id"),
FOREIGN KEY("tid") REFERENCES "tags"("id")
);
CREATE TABLE IF NOT EXISTS "hashes" (
"id" INTEGER,
"md5" text,
"fid" int,
"md5" TEXT NOT NULL,
"fid" INTEGER NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("fid") REFERENCES "files"("id")
);

234
tag.sh
View File

@ -1,29 +1,63 @@
#!/usr/bin/env bash
set -ueo pipefail
[ "${DEBUG:-0}" = "1" ] && set -x
# usage:
# - init - create an empty database
# - import - import $FILE or import $DIR, both can be multiple values
# - autoimport ($REGEX|video) $FOLDER - will import all files in $FOLDER with a $REGEX filter,
# or one of the preset filters:
# - video
# - add (tag|filename) $VALUE - adds a new tag or filename of $VALUE
# - list (tags|files) - lists all the values in a table
# - bytag $TAG - list all files by tag label
# - tag $FILENAME $TAG - tag $FILENAME with $TAG, $TAG can be repeated
# - listtags $FILENAME - list all tags for $FILENAME
: <<'DOCS'
=head1 NAME
# TODO: when tagging, check if tags exist
# TODO: when tagging, allow more than one tag
# FIXME: adding tags doesn't work
tags is a tool for keeping file tags in a SQLite database.
SCRIPT_DIR=$(dirname $(readlink "$0"))
CURRENT_DIR=$(dirname "$PWD")
=head1 SYNOPSIS
tag [OPTIONS] <init|import|add|list|bytag|listtags>
=head1 OPTIONS
=over 4
=item B<init>
Create an empty database.
=item B<import> I<filename>
Import filename, can be single value or multiple values.
=item B<autoimport> I<regexp> I<path>
Import all files in I<path>. Can be filtered using custom I<regexp>, or one of the regular expression presets (currently only "video").
=item B<add> I<type> I<value>
Adds a new item of the specified I<type>: C<tag>, C<file>, C<hash>.
=item B<list> I<type> I<[--bytag|--byfile]>
List C<tags>, C<files>, C<hashes> (these are all table names, internally).
=item B<tag> I<[-i|--interactive]> I<--file|--id> I<tag>
Tags I<filename> with one or more I<tag>.
=back
=head1 TODO
* adjust sqlite_insert_multiple to allow defining columns per insert, currently it's hardcoded to 2
* merge sqlite_insert_single and sqlite_insert_multiple probably as result of the above
* listtags doesn't work with full path
* listtags doesn't work after tagging a file
=head1 LICENSE AND COPYRIGHT
=cut
DOCS
SCRIPT_DIR=$(dirname "$(readlink "$0")")
SCRIPT_NAME=$(basename "$0")
DB_FILE="${CURRENT_DIR}/tags.db"
[ "$1" = "--db" ] && DB_FILE="$2" && shift 2
DB_FILE="${PWD}/tags.db"
[ "${1:-}" = "--db" ] && DB_FILE=$(readlink -f "${2/#~/$HOME}") && shift 2
DB_SCHEMA="${SCRIPT_DIR}/database.sql"
@ -43,7 +77,7 @@ log() {
init() {
# DB_NAME
[ -f "$1" ] && fail "Database file \"$1\" already exists. Aborting."
cat "$DB_SCHEMA" | sqlite3 "$1"
sqlite3 "$1" < "$DB_SCHEMA"
log "Empty database file \"$1\" created."
}
@ -61,16 +95,18 @@ listtags() {
[ -z "$1" ] && fail "No filename supplied."
FILENAME="$1"
shift
LIMIT="${1:-0}"
# ! file_exists_in_db "$FILENAME" && fail "File '$FILENAME' does not exist in database."
LIMIT="${1:-0}"
ADDITIONAL_QUERY=""
[ $LIMIT -gt 0 ] && ADDITIONAL_QUERY="LIMIT $LIMIT"
sqlite_query \
"SELECT filename, label from files \
INNER JOIN tags_ties ON tags_ties.id = files.id \
INNER JOIN tags ON tags.id = tags_ties.id \
WHERE filename = \"$FILENAME\"\
INNER JOIN tags_ties ON tags_ties.fid = files.id \
INNER JOIN tags ON tags.id = tags_ties.tid \
WHERE filename LIKE \"%$FILENAME%\"\
$ADDITIONAL_QUERY"
}
@ -78,13 +114,16 @@ add() {
# $TYPE(tag|path|hash) $VALUE1..$VALUEN
[ "$1" = "tag" ] && shift && add_tag "$@" && exit 0
[ "$1" = "path" ] && shift && add_path "$@" && exit 0
[ "$1" = "hash" ] && fail "Adding hashes is not implemented yet." && TABLE="hashes" && COLUMN="md5,filename"
exit 0
[ "$1" = "hash" ] && shift && add_hash "$@" && exit 0
[ "$1" = "file" ] && fail "Use \"$SCRIPT_NAME add path\" instead."
pod2usage "$0"
exit 1
}
sqlite_query() {
# $QUERY
sqlite3 -table "$DB_FILE" "$1"
dbfile_exists || fail "Database file '$DB_FILE' does not exist. Use '$SCRIPT_NAME init' to create default one, or specify filename with '--db'."
sqlite3 "$DB_FILE" "$1"
}
sqlite_lastrows() {
@ -92,7 +131,7 @@ sqlite_lastrows() {
sqlite_query "SELECT * FROM \"$1\" ORDER BY id DESC LIMIT ${2}"
}
sqlite_insert() {
sqlite_insert_single() {
# $TABLE $COLUMN $VALUES
# $VALUES can be comma-delimited
[ -z "$1" ] && fail "No table specified."
@ -109,7 +148,29 @@ sqlite_insert() {
VALUES+=",(\"$1\")"
done
fi
QUERY="INSERT INTO $TABLE ($COLUMN) VALUES ${VALUES} RETURNING *;"
QUERY="PRAGMA foreign_keys=ON;"
QUERY+="INSERT INTO $TABLE ($COLUMN) VALUES ${VALUES} RETURNING *;"
sqlite_query "$QUERY"
}
sqlite_insert_multi() {
# $TABLE $COLUMN $VALUE1 $VALUE2
[ -z "${1:-}" ] && fail "No table specified."
TABLE="$1"
[ -z "${2:-}" ] && { pod2usage "$0"; exit 1 ; }
COLUMN="$2"
[[ -z "${3:-}" || -z "${4:-}" ]] && { pod2usage "$0"; exit 1 ; }
shift 2
VALUES="($1,$2)"
if [[ ! -z "${3:-}" && ! -z "${4:-}" ]]; then
while true; do
shift 2
[[ -z "${1:-}" && -z "${2:-}" ]] && break
VALUES+=",($1,$2)"
done
fi
QUERY="PRAGMA foreign_keys=ON;"
QUERY+="INSERT INTO $TABLE ($COLUMN) VALUES ${VALUES} RETURNING *;"
sqlite_query "$QUERY"
}
@ -117,27 +178,90 @@ add_tag() {
# $TAG1...$TAGN
TABLE="tags"
COLUMN="label"
sqlite_insert "$TABLE" "$COLUMN" "$@"
sqlite_insert_single "$TABLE" "$COLUMN" "$@"
}
add_path() {
# $PATH1..$PATHN
TABLE="files"
COLUMN="filename"
sqlite_insert "$TABLE" "$COLUMN" "$@"
COLUMN="filename,path"
FILES=()
for FILE in "$@"; do
[ ! -f "$FILE" ] && fail "File '$FILE' does not exist in the specified path."
local FILENAME=$(basename "$FILE")
local FILEPATH=$(readlink -f "$(dirname "$FILE")")
file_exists_in_db "$FILE" && fail "File '$FILE' already exists in database."
FILES+=("\"$FILENAME\"")
FILES+=("\"$FILEPATH\"")
done
sqlite_insert_multi "$TABLE" "$COLUMN" "${FILES[@]}"
}
add_hash() {
# $FILE $HASH
local TABLE="hashes"
local COLUMN="fid,md5"
local FILENAME="${1:-}"
[ -z "$FILENAME" ] && fail "No file specified."
local FID=$(id_by_filename "$FILENAME")
[[ "$FID" -eq 0 ]] && fail "File \"$FILENAME\" does not exist in database."
sqlite_insert_multi "$TABLE" "$COLUMN" $FID "'$2'"
}
add_path_auto() {
# $REGEX $FOLDER
find "${2}" -type f -regextype posix-extended -iregex "$1" -exec "$0" import "{}" +
find "${2}" -type f -regextype posix-extended -iregex "$1" -exec "$0" --db "$DB_FILE" import "{}" +
}
tag_exists_in_db() {
# $TAGLABEL
TAG=${1:-}
RESULT=$(sqlite_query "SELECT id FROM tags WHERE label = \"$TAG\"")
if [[ -z "$RESULT" ]]; then
return 1
else
return 0
fi
}
file_exists_in_db() {
# $FILENAME
local RESULT=$(id_by_filename "$1")
if [[ "$RESULT" -eq 0 ]]; then
return 1
else
return 0
fi
}
id_by_filename() {
# FILENAME
local FILENAME=$(basename "${1:-}")
local FILEPATH=$(readlink -f "$(dirname "${1:-}")")
local RESULT=0
RESULT=$(sqlite_query "SELECT id FROM files WHERE filename = \"$FILENAME\" AND path = \"$FILEPATH\"")
echo $RESULT
}
dbfile_exists() {
if [[ ! -f "$DB_FILE" ]]; then
return 1
else
return 0
fi
}
main() {
[ -z "${1:-}" ] && { pod2usage "$0"; exit 1; }
if [[ "$1" = "init" ]]; then
init "${2:-$DB_FILE}"
exit 0
fi
[ ! -f "$DB_FILE" ] && fail "Database file \"$DB_FILE\" does not exist."
if [[ "$1" = "import" ]]; then
shift
add "path" "$@"
@ -158,30 +282,39 @@ main() {
fi
if [[ "$1" = "list" ]]; then
[ -z "$2" ] && fail "No table supplied."
[ -z "${2:-}" ] && { pod2usage "$0"; exit 1 ; }
TABLE_NAME="$2"
sqlite3 -table "$DB_FILE" "SELECT * FROM \"${TABLE_NAME}\""
sqlite_query "SELECT * FROM \"${TABLE_NAME}\""
exit 0
fi
if [[ "$1" = "bytag" ]]; then
[ -z "$2" ] && fail "No tag supplied."
TAG_NAME="$2"
sqlite3 -table "$DB_FILE" "SELECT filename FROM files WHERE id = (SELECT id FROM tags_ties WHERE id = (SELECT id FROM tags WHERE label = \"${TAG_NAME}\"))"
sqlite_query "SELECT filename FROM files WHERE id = (SELECT id FROM tags_ties WHERE id = (SELECT id FROM tags WHERE label = \"${TAG_NAME}\"))"
exit 0
fi
if [[ "$1" = "tag" ]]; then
shift
[ -z "$1" ] && fail "No filename supplied."
if [[ "${1:-}" = "-i" || "${1:-}" = "--interactive" ]]; then
"$0" --db "$DB_FILE" tagfid \
$(sqlite_query "SELECT id,filename,path FROM files" | fzf | xargs -I{} echo '{}' | awk -F'|' '{print $1}') \
$(sqlite_query "SELECT label FROM tags" | fzf --multi | xargs -I{} echo '{}' | awk -F'|' '{print $1}')
exit 0
fi
[ -z "${1:-}" ] && fail "No filename supplied."
FILENAME="$1"
! file_exists_in_db "$FILENAME" && fail "File '$FILENAME' does not exist in database."
shift
[ -z "$1" ] && fail "No tag supplied."
[ -z "${1:-}" ] && fail "No tag supplied."
COUNTER=0
while true; do
LABEL="$1"
! tag_exists_in_db "$LABEL" && fail "Tag '$TAG' does not exist in database."
shift
sqlite3 -table "$DB_FILE" "INSERT INTO tags_ties (id, id) VALUES ((SELECT id FROM files WHERE filename = \"$FILENAME\"),(SELECT id FROM tags WHERE label = \"$LABEL\"))"
local FID=$(id_by_filename "$FILENAME")
sqlite_query "INSERT INTO tags_ties (fid, tid) VALUES ($FID,(SELECT id FROM tags WHERE label = \"$LABEL\"))"
COUNTER=$((COUNTER++))
[ -z "${1:-}" ] && break
done
@ -189,11 +322,34 @@ main() {
exit 0
fi
if [[ "$1" = "tagfid" ]]; then
shift
[ -z "${1:-}" ] && fail "No file ID supplied."
local FID="$1"
shift
[ -z "${1:-}" ] && fail "No tag supplied."
COUNTER=0
while true; do
LABEL="$1"
! tag_exists_in_db "$LABEL" && fail "Tag '$TAG' does not exist in database."
shift
sqlite_query "INSERT INTO tags_ties (fid, tid) VALUES ($FID,(SELECT id FROM tags WHERE label = \"$LABEL\"))"
COUNTER=$((COUNTER++))
[ -z "${1:-}" ] && break
done
exit 0
fi
if [[ "$1" = "listtags" ]]; then
[ -z "${2:-}" ] && fail "Usage: $SCRIPT_NAME listtags filename"
[ -z "${2:-}" ] && { pod2usage "$0"; exit 1 ; }
listtags "$2"
exit 0
fi
if [[ "$1" = "listbyfile" ]]; then
sqlite_query "SELECT id,filename FROM files" |\
fzf --delimiter="|" --preview "sqlite3 $DB_FILE 'SELECT label FROM tags WHERE id IN (SELECT tid FROM tags_ties WHERE fid ={2})'"
fi
}
main "$@"

View File

@ -3,6 +3,7 @@ set -ueo pipefail
SCRIPT_DIR=$(dirname $(readlink -f "$0"))
DB_FILE="test.db"
FILENAME="test.mp4"
oneTimeSetUp() {
tag --db "$DB_FILE" init >/dev/null
@ -14,6 +15,7 @@ oneTimeTearDown() {
# runs one-time teardown two times
# see https://github.com/kward/shunit2/issues/112
[ -f "$DB_FILE" ] && rm "$DB_FILE"
[ -f "$FILENAME" ] && rm "$FILENAME"
}
testDbCreated() {
@ -23,21 +25,49 @@ testDbCreated() {
testTagAdded() {
tag --db "$DB_FILE" add tag hello >/dev/null
RESULT=$(sqlite3 "$DB_FILE" "SELECT label from tags WHERE label = 'hello'")
assertEquals $RESULT "hello"
assertEquals "hello" "$RESULT"
}
testFailAddingNonexistentFile() {
tag --db "$DB_FILE" import "$FILENAME" 2>/dev/null
assertEquals 1 "$?"
}
testFilenameAdded() {
local FILENAME="test.mp4"
touch "$FILENAME"
tag --db "$DB_FILE" add path "$FILENAME" >/dev/null
RESULT=$(sqlite3 "$DB_FILE" "SELECT filename from files WHERE filename = '$FILENAME'")
assertEquals "$RESULT" "$FILENAME"
assertEquals "$FILENAME" "$RESULT"
}
testHashAdded() {
local HASH="fbe2153ce0614d76a378b2e6fe07cc9e"
tag --db "$DB_FILE" add hash "$HASH" >/dev/null
RESULT=$(sqlite3 "$DB_FILE" "SELECT md5 from hashes WHERE hash = '$HASH'")
assertEquals "$RESULT" "$HASH"
tag --db "$DB_FILE" add hash "$FILENAME" "$HASH" >/dev/null
RESULT=$(sqlite3 "$DB_FILE" "SELECT md5 from hashes WHERE md5 = '$HASH'")
assertEquals "$HASH" "$RESULT"
}
testFailAddingHashOnNonexistentFile() {
local HASH="fbe2153ce0614d76a378b2e6fe07cc9e"
tag --db "$DB_FILE" add hash "nonexistentfile" "$HASH" 2>/dev/null
assertEquals 1 "$?"
}
testTagFile() {
tag --db "$DB_FILE" tag "$FILENAME" hello >/dev/null
RESULT=$(sqlite3 "$DB_FILE" "SELECT label FROM tags WHERE id = (SELECT tid from tags_ties WHERE fid = (SELECT id FROM files WHERE filename = '$FILENAME'))")
assertEquals "hello" "$RESULT"
}
testListingTags() {
tag --db "$DB_FILE" listtags "$FILENAME" | grep -q "^$FILENAME|hello$"
assertEquals "$?" 0
}
testAddingMultipleTags() {
tag --db "$DB_FILE" add tag more more2 >/dev/null
RESULT=$(sqlite3 -list "$DB_FILE" "SELECT COUNT(*) from tags WHERE label IN ('hello', 'more', 'more2')")
assertEquals 3 "$RESULT"
}
. ../shunit2/shunit2