jlh's assorted stuff

Pretty random

Your own web clipboard in 10 minutes

I used to be a big fan of www.kl1p.com which allows you to quickly copy & paste texts, URLs and other short fragments between different devices. You can even make up your own URLs on-the-fly by just appending something at the end, e.g. www.kl1p.com/mystuff. But as soon as you have to copy around some sensitive or personal data it suddenly makes you wonder if perhaps someone else will end up reading it, even if you carefully choose a random string for the URL and delete the content immediately after using it. And that site doesn’t even have HTTPS, so anyone on the wire can read your content. Oh and then you also notice that when opening it on Chrome on Android, you can’t actually select the text in order to copy it and it makes you wonder why oh why doesn’t this work?

But luckily such a service is so trivial to create that surely someone else made a better version of it, right? Let’s try wepaste.com shall we? But wait, if you paste emojis into that one then the content will fail to reload on at least some browsers and there’s still no HTTPS. It’s not like it’s 2018 already and HTTPS is just a luxury, right? And it also sucks on mobile.

How hard can it be to create your own service for just quickly copy pasting between devices and be sure no one else will read it? It turns out that if you keep it simple it’s just about 10 minutes. Here’s my quick version in PHP:

<?php

$data = file_get_contents('data');

?><!DOCTYPE html>
<html>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <style type='text/css'>
        textarea {
            display: block;
            box-sizing: border-box;
            width: 100%;
            height: 200px;
            margin-bottom: 10px;
            border: none;
        }

        button {
            float: right;
        }

        input, button {
            padding: 10px 20px;
        }
    </style>
</head>
<body>
<form action='' method='POST'>
    <textarea name='text' id='text' placeholder='Your notes...'><?php echo htmlspecialchars($data); ?></textarea>
    <input type='submit' value='Save'>
    <button type='button' onclick='document.getElementById("text").value="";'>Clear</button>
</form>
</body>
</html>
<?php

if (array_key_exists('text', $_POST)) {
    file_put_contents('data', $_POST['text']);
    header('Location: .');
}

Deploy this under any name you like and make sure to create the ‘data’ file in advance, do chown www-apache.www-apache data and you’re done. It’s beautifully simple, works on mobile devices, see it deployed here: https://jas.atdot.ch/mystuff/.

Mind you, it lacks many features, for instance there’s only one clipboard, not multiple. But I need only one for myself, so it’s fine. Many areas could be vastly improved, but the point is that it work just fine as long as you deploy this on a HTTPS-enabled webserver with a hard-to-guess URL or even add apache2/nginx/whatever basic authentication for even more security. And for your personal use it will end up being vastly superior to those other sites.

Directory renamer script (dirrename)

Did you ever have to rename many files at once, using a certain pattern? For example rename “DSCXXX.JPG” to “Picture XXX.jpg”, where XXX goes from 001 to 123? Of course you could write a shell one-liner to do the job, but it’s much simpler and quicker if you can load the list of file names into your favourite text editor and then use standard editor features (search & replace) to rename the files. This is exactly what this script does: You call it without arguments and it loads all file names of the current directory into $EDITOR, let’s you edit them, and when you’re done, it renames all files accordingly. It’s simple, yet powerful, even though it lacks many bells and whistles yet. It correctly handles cycles, so you can exchange two file names if you like.

For convenience, copy the following into /usr/local/bin/dirrename and make it executable:

#!/usr/bin/perl -w

# Copyright (c) 2008 jlh (jlh at gmx dot ch)
#
# This bit of code is released under the GPL version 2 or (at your option) any
# later version.

use strict;
use warnings;

my $get_tmp_name_n = 0;

sub get_tmp_name {
	my $tmp;

	while (1) {
		$get_tmp_name_n++;
		$tmp = ".__dirrename_tmp_${get_tmp_name_n}__";
		last if !-e $tmp;
	}

	return $tmp;
}

# check if we have write access to .

if (!-w '.') {
	print "I do not have write access to this directory.\n";
	exit 1;
}

# read the list of files to rename

opendir my $h, '.' or die;
my @list = sort grep { !/^\./ } readdir $h;
closedir $h;

# write it to a file

my $tmp = "/tmp/pid.$";
open $h, '>', $tmp or die;
print $h join("\n", @list), "\n";
close $h;

# let the user edit it

my $editor = $ENV{EDITOR} || 'vi';
system("$editor '$tmp'");

# read it back in

open $h, '<', $tmp or die;
my @newlist = map { my $a = $_; chomp $a; $a; } <$h>;
close $h;
unlink $tmp;

# compare

if (@list != @newlist) {
	print "Number of lines has been changed!";
	exit 1;
}

# check for errors

my $error = 0;

my %from;
$from{$_}++ for @list;
my %to;
$to{$_}++ for @newlist;

for (keys %to) {
	if ($to{$_} > 1) {
		print "error: more than one file wants to rename to '$_'\n";
		$error = 1;
	}
	if (!exists $from{$_} and -e $_) {
		print "error: target exists already: $_\n";
		$error = 1;
	}
}

exit $error if $error;

# build a trivial strategy to rename the file (rename all files to a temporary
# name, then all temporary names are renamed to the target names; this avoids
# cycles and dependencies)

my @strategy;

for (0 .. $#list) {
	my ($old, $new) = ($list[$_], $newlist[$_]);
	next if $old eq $new;
	my $tmp = &get_tmp_name;
	unshift @strategy, [ $old, $tmp ];
	push @strategy, [ $tmp, $new ];
}

# now optimize this strategy.  this avoids unnecessary renaming but takes more
# time to plan.  it would probably be faster without doing this, but it was fun
# to write it.  :)

while (1) {

	my $done_something = 0;

	for (my $i = 0; $i < @strategy; $i++) { # regular loop because we splice @strategy in here
		my $target = $strategy[$i][1];
		my $j = undef;
		for ($i + 1 .. $#strategy) {
			if ($strategy[$_][1] eq $target) {
				die "Assertion failure...\n";
			}
			if ($strategy[$_][0] eq $target) {
				$j = $_;
				last;
			}
		}
		if (defined $j) {
			my $target_target = $strategy[$j][1];

			# see if we can merge $strategy[$i] and $strategy[$j] into $strategy[$i]...

			my $ok = 1;
			for ($i + 1 .. $j - 1) {
				if ($strategy[$_][0] eq $target_target or $strategy[$_][1] eq $target_target) {
					$ok = 0;
					last;
				}
			}

			if ($ok) {
				# ...yes we can, do it
				#print "reducing chain: $strategy[$i][0] -> $target -> $target_target (up)\n";
				$strategy[$i][1] = $target_target;
				splice @strategy, $j, 1;
				next;
			} 

			# we couldn't, try to merge $strategy[$i] and $strategy[$j] into $strategy[$j] instead

			my $from = $strategy[$i][0];
			$ok = 1;
			for ($i + 1 .. $j - 1) {
				if ($strategy[$_][0] eq $from or $strategy[$_][1] eq $from) {
					$ok = 0;
					last;
				}
			}

			if ($ok) {
				# ...yes we can, do it
				#print "reducing chain: $strategy[$i][0] -> $target -> $target_target (down)\n";
				$strategy[$j][0] = $from;
				splice @strategy, $i, 1;
				$i--;
				next;
			}
		}
	}

	last unless $done_something;
}

# execute it

print "Performing " . scalar(@strategy) . " rename operations\n";

for (@strategy) {
	#print "$_->[0] -> $_->[1]\n";
	rename $_->[0], $_->[1];
}