webnomad/index

436 lines
12 KiB
Bash
Executable File

#!/usr/bin/env zsh
#
# ZShelf, file directory indexing for the web
#
# Copyright (C) 2014 Denis Roio <jaromil@dyne.org>
#
# This source code is free software; you can redistribute it and/or
# modify it under the terms of the GNU Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# Please refer to the GNU Public License for more details.
#
# You should have received a copy of the GNU Public License along with
# this source code; if not, write to:
# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
VERSION=0.1
QUIET=0
RECURSIVE=0
PARAM=()
autoload colors; colors
# standard output message routines
# it's always useful to wrap them, in case we change behaviour later
notice() { if [[ $QUIET == 0 ]]; then print "$fg_bold[green][*]$fg_no_bold[default] $1" >&2; fi }
error() { if [[ $QUIET == 0 ]]; then print "$fg[red][!]$fg[default] $1" >&2; fi }
func() { if [[ $DEBUG == 1 ]]; then print "$fg[blue][D]$fg[default] $1" >&2; fi }
act() {
if [[ $QUIET == 0 ]]; then
if [ "$1" = "-n" ]; then
print -n "$fg_bold[white] . $fg_no_bold[default] $2" >&2;
else
print "$fg_bold[white] . $fg_no_bold[default] $1" >&2;
fi
fi
}
# quick bold
B="$fg_bold[white]"
r="$reset_color"
# honor quiet and debug flags as early as possible
if [[ ${@} == *-q* ]]; then QUIET=1; fi
if [[ ${@} == *-D* ]]; then DEBUG=1; fi
# what operating system are we in? use os_detect()
# simplifying modes of operation: GNU or MAC
case $(uname) in
Linux) OS=GNU
notice "ZShelf v$VERSION running on GNU/Linux" ;;
Darwin) OS=MAC
notice "ZShelf v$VERSION running on Mac/OSX" ;;
*) OS=GNU # default
error "Running on an unknown operating system, assuming GNU" ;;
esac
if [[ ${@} == *-r* ]]; then RECURSIVE=1; fi
# file list map for current dir
# format: filename;size;date
typeset -alU files
# TODO
usage() { act "no help ATM sry" }
filetype_icon() {
{ test "$1" = "" } && { error "filetype_icon called without argument"; return 1 }
path=""
filename="$1"
{ test -r "$1" } && {
act "fuletype_icon: argument is not a real path, skipping mimecheck"
path="$1"
filename="${path##*/}"
}
ext="${filename##*.}"
name="${filename%%.*}"
res="default.jpg"
# analize extensions, will be overridden by name
case $ext:l in
txt) res=text-plain.jpg ;;
html) res=text-x-html.jpg ;;
xml) res=text-xml.jpg ;;
md5|sha*) res=hash.jpg ;;
asc|gpg) res=signature.jpg ;;
tex|md|org) res=text-x-texinfo.jpg ;;
patch) res=text-x-patch.jpg ;;
sql) res=text-x-sql.jpg ;;
vcart) res=text-x-vcard.jpg ;;
dmg) res=dmg.png ;;
esac
# analize name
case $name:l in
changelog) res=text-x-changelog.jpg ;;
readme) res=text-x-readme.jpg ;;
install) res=text-x-install.jpg ;;
authors) res=text-x-authors.jpg ;;
makefile) res=text-x-makefile.jpg ;;
news|usage) res=text-x-nfo.jpg ;;
copying) res=text-x-copying.jpg ;;
known_bugs|todo) res=text-x-log.jpg ;;
esac
# check physical file properties
{ test "$path" = "" } || {
# is a directory
{ test -d "$path" } && {
case $name:l in
sources) res=folder-development.jpg ;;
doc|docs) res=folder-documents.jpg ;;
releases|binaries) res=folder-downloads.jpg ;;
esac
}
# todo using mimetype -b and more
}
}
index_dir() {
{ test -d "$1" } || { error "cannot index: not a directory '$1'"; return 1 }
files=()
ttmp=`ls -l --time-style=long-iso "$1" | awk '
/^total/ { next }
/^$/ { next }
{ printf "files+=(\"%s;%s;%s\")", $8, $5, $6 }
'`
{ test $? = 0 } || {
error "Error parsing directory: $1"
return 1 }
eval "$ttmp"
act "${#files} files parsed in $1"
# setup paths for test
{ test "$destination" = "test" } && {
LINK_PREFIX="file://$1"
act "LINK_PREFIX = $LINK_PREFIX"
}
# human size dividers
_mb=$((1024 * 1024))
_gb=$((1024 * 1024 * 1024))
cat <<EOF
<table class="table table-hover table-condensed">
<thead><tr>
<th class="col-sm-1 col-md-1 col-lg-1"><!-- filetype icon --></th>
<th class="col-sm-4 col-md-4 col-lg-4">Filename</th>
<th class="col-sm-1 col-md-2 col-lg-3">Size</th>
<th class="col-sm-1 col-md-2 col-lg-3">Date</th>
<th class="col-sm-3 col-md-4 col-lg-5">Preview</th>
</tr></thead>
EOF
for f in ${files}; do
name="${f[(ws:;:)1]}"
size="${f[(ws:;:)2]}"
date="${f[(ws:;:)3]}"
ext="${name##*.}" # file extension
file="${1}/${name}" # file path
# format size to human readable form
if [[ $size -lt 1024 ]]; then hsize="$size B"
elif [[ $size -gt 1024 ]]; then hsize="$(( $size / 1024 )) KB"
elif [[ $size -gt 1048576 ]]; then hsize="$(( $size / $mb )) MB"
else hsize="$(( $size / $gb )) GB"
fi
# format date to human readable form
hdate=`date -d "$date" +'%d %b %Y'`
func "$name \t $size \t $date"
typefield=""
namefield=""
previewfield=""
link=""
icon=""
preview=`preview_file "$file"`
{ test "$preview" = "" } || {
func "file preview produced"
case $ext:l in
jpg|jpeg|png|gif|pdf|svg|eps)
func "preview is a thumbnail"
preview="<img src=\"$preview\" alt=\"$name\" title=\"$name\">"
icon="image.jpg"
;;
esac
}
icon_width="width=\"50px\""
if [ -L "$file" ]; then # is a symlink
if [ -d "$file" ]; then # symlink to folder
link="<a href=\"${name}/index.html\">"
typefield="${link}<img src=\"${WEB_ROOT}/icons/symlink.jpg\" alt=\"symlink\" ${icon_width}></a>"
namefield="${link}${name}</a>"
previewfield="<!-- folder -->"
else # symlink to file
link="<a href=\"${LINK_PREFIX}/${link}\">"
typefield="${link}<img src=\"${WEB_ROOT}/icons/symlink.jpg\" alt=\"symlink\" ${icon_width}></a>"
namefield="${link}${name}</a>"
previewfield="${link}${preview}</a>"
fi
elif [ -d "$file" ]; then # is a folder
link="<a href=\"${name}/index.html\">"
typefield="${link}<img src=\"${WEB_ROOT}/icons/folder.jpg\" alt=\"folder\" ${icon_width}></a>"
namefield="${link}${name}</a>"
previewfield="<!-- folder -->"
else # is a file
{ test "$icon" = "" } && { icon="`filetype_icon ${file}`" }
link="<a href=\"${LINK_PREFIX}/${link}\">"
typefield="${link}<img src=\"${WEB_ROOT}/icons/${icon}\" alt=\"${icon}\" ${icon_width}></a>"
namefield="${link}${name}</a>"
previewfield="${link}${preview}</a>"
fi
# render it all
cat <<EOF
<tr>
<td style="vertical-align:middle;">${typefield}</td>
<td style="vertical-align:middle;font-size:2em">${namefield}</td>
<td style="vertical-align:middle">$hsize</td>
<td style="vertical-align:middle">$hdate</td>
<td style="vertical-align:middle">${previewfield}</td>
</tr>
EOF
done
print "</table>"
}
preview_file() {
{ test "$1" = "" } && { error "no file to preview."; return 1 }
# get the file extension using zsh builtins
# %% is for deleting prefix and ## is for deleting suffix
for f in ${1}; do
filename="${f##*/}"
name="${filename%%.*}"
ext="${f##*.}"
# lowercase
case $ext:l in
md) # markdown
notice "$f: rendering markdown using maruku"
output="$name".html
maruku --html --html-frag $f -o "$output"
print "$output"
;;
jpg|jpeg|png)
output="${name}-thumb.${ext}"
{ test -r "$output" } && {
act "$f: thumbnail found, skip rendering"
print "$output"
continue }
notice "$f: rendering thumbnail using ImageMagick"
convert $f -resize 100 $output
print "$output"
;;
pdf|gif) # only first page / animation frame
output="${name}-thumb.jpg"
{ test -r "$output" } && {
act "$f: thumbnail found, skip rendering"
print "$output"
continue }
notice "$f: rendering thumbnail using ImageMagick"
convert "${f}[0]" -resize 100 $output
print "$output"
;;
svg|eps)
output="${name}-thumb.jpg"
{ test -r "$output" } && {
act "$f: thumbnail found, skip rendering"
print "$output"
continue }
notice "$f: rendering thumbnail using ImageMagick"
convert -density 144 "${f}" -resize 100 $output
print "$output"
;;
esac
done
return 0
}
autostart() {
if [ -d "$1" ]; then
index_dir "$1"
elif [ -f "$1" ]; then
preview_file "$1"
else
return 1
fi
return $?
}
typeset -A subcommands_opts
### Options configuration
#Hi, dear developer! Are you trying to add a new subcommand, or to add some options?
#Well, keep in mind that:
# 1. An option CAN'T have differente meanings/behaviour in different subcommands.
# For example, "-s" means "size" and accept an argument. If you are tempted to add
# an option "-s" (that means, for example "silent", and doesn't accept an argument)
# DON'T DO IT!
# There are two reasons for that:
# I. usability; user expect that "-s" is "size
# II. Option parsing WILL EXPLODE if you do this kind of bad things
# (it will say "option defined more than once, and he's right)
# recursive, quiet, Debug, help, verbose, n=dryrun
main_opts=(r q D h v n)
# p: prefix (string arg) = <a href> link prefix
subcommands_opts[index]="p: "
subcommands_opts[preview]=""
option_is_set() {
#First argument, the option (something like "-s")
#Second (optional) argument: if it's "out", command will print it out 'set'/'unset'
# This is useful for if conditions
#Return 0 if is set, 1 otherwise
[[ -n ${(k)opts[$1]} ]];
r=$?
if [[ $2 == out ]]; then
if [[ $r == 0 ]]; then print 'set'
else print 'unset'; fi
fi
return $r;
}
option_value() { #First argument, the option (something like "-s")
<<< ${opts[$1]}
}
### Detect subcommand
local -aU every_opts #every_opts behave like a set; that is, an array with unique elements
for optspec in $subcommands_opts$main_opts; do
for opt in ${=optspec}; do
every_opts+=${opt}
done
done
local -a oldstar
oldstar=($argv)
zparseopts -M -E -D -Adiscardme ${every_opts}
unset discardme
subcommand=$1
{ test "$subcommand" = "" } && { subcommand="__default" }
argv=(${oldstar})
unset oldstar
### Parsing global + command-specific options
# zsh magic: ${=string} will split to multiple arguments when spaces occur
set -A cmd_opts ${main_opts} ${=subcommands_opts[$subcommand]}
if [[ -n $cmd_opts ]]; then #if there is no option, we don't need parsing
zparseopts -M -E -D -Aopts ${cmd_opts}
if [[ $? != 0 ]]; then
error "Some error occurred during option processing."
exitcode=1
return 1
fi
fi
#build PARAM (array of arguments) and check if there are unrecognized options
local ok=0
for arg in $*; do
if [[ $arg == '--' || $arg == '-' ]]; then
ok=1
continue #it shouldnt be appended to PARAM
elif [[ $arg[1] == '-' ]]; then
if [[ $ok == 0 ]]; then
error "unrecognized option $arg"
exitcode=1
return 1
fi
fi
PARAM+=$arg
done
# if 1st parameter is a known subcommand omit it from params
{ test "$subcommand" = "__default" } || { PARAM[1]=(); shift }
### End parsing command-specific options
{ option_is_set -v } && {
cat $ZSHELFEXEC | awk '/^#/ {print $0 } !/^#/ {exit}'
echo }
{ option_is_set -h } && { usage; return 0 }
{ option_is_set -v } && { CLEANEXIT=0
cat $JAROMAILEXEC | awk 'BEGIN { v=1 } !/^#/ { exit }'
return 0 }
{ option_is_set -q } && { QUIET=1 }
{ option_is_set -D } && { DEBUG=1; QUIET=0; func "All debug messages ON" }
{ option_is_set -n } && { DRYRUN=1; act "Dry run, show operations without executing them." }
{ option_is_set -r } && { RECURSIVE=1 }
{ option_is_set -p } && { LINK_PREFIX="`option_value -p`" }
# we take a file or directory as the only one parameter
ARG=${PARAM[1]}
case "$subcommand" in
index)
index_dir "$ARG"
{ test -r "${ARG}/README.md" } && {
res=`preview_file "${ARG}/README.md"`
print "<hr>"
cat "$res"
}
;;
preview) preview_file ${PARAM} ;;
help) usage ;;
'source') return 0 ;;
*) # unknown command, pass it to autostart
func "unknown command, using autostart"
autostart ${=PARAM}
exitcode=$?
{ test $exitcode = 0 } || {
error "command \"$subcommand\" not recognized"
act "try -h for help"
}
;;
esac
exitcode=$?
return $exitcode