I have a 5-year-old Brother Scanner (the ADS-1700W) that serves me pretty well. It is small enough to keep on my desk, fast, and has a Wi-Fi connection. But getting scans from the scanner to the computer is sometimes bothersome. My primary way of using it is to scan to a network folder on my home server, which is great for archiving things, but not so great for family members or when I need something right away.
My preferred way to get a quick scan is by email. You immediately have it on whatever device you are using, and you have it saved for later if you need it. The Brother Scanner has a scan-to-email function, but it is buggy. Specifically, it sends slightly malformed emails that Fastmail accepts but Gmail returns to sender.
But since network scanning is rock solid, last year I wrote a program to watch a set of folders and send by email whatever files it finds. I was on a bit of a Go kick at the time, and I think Go works pretty well for the task.
Here is the relatively short program that I wrote. It is meant to be running as a daemon, and as long as it is running, it will email you all the files that it finds. Since the interesting parts are at the end and I don’t think anyone will read that far, I’ll show you the program in reverse order, starting with the main function.
funcmain(){fmt.Println("Starting up")changes:=make(chanint)gowaitForChange(changes)ticks:=time.Tick(5*time.Minute)for{err:=filepath.Walk("/home/scanner/incoming",sendFile)iferr!=nil{fmt.Println(err)}select{case<-changes:fmt.Println("File changed, looking for emails to send")case<-ticks:fmt.Println("5m has passed, looking for emails to send")}}}
We start up, print a nice message and make a channel, which is a Go structure that allows us to pass data, in this case integers, between different threads of the program. We call it changes because it will notify us every time there has been a filesystem change.
The go keyword on line 99 starts waitForChange on a separate thread, which is a good thing for us, because you will later see that it runs an infinite loop. We pass it the channel so that it can notify us when it sees a change.
On line 101 we get a Ticker channel, which will receive a signal every five minutes. Since I don’t completely trust that I will be notified every time a file changes, every once in a while we want to look through the directories to see if we find anything.
Starting at line 102, we have an infinite loop here in the main thread. This starts by mailing out any files that are waiting. Then we get to the select statement, which pauses and listens to both the changes channel, and the ticks channel. The somewhat strange arrow syntax means that we are attempting to read values from the channel. If we wanted, we could assign the values we read to a variable and do something with them, but we don’t care what is on the channels. As soon as another thread writes to one of these channels (whichever channel comes first), we write the corresponding log statement and then continue back to the top of the for loop, which mails out any files we find, and then goes back to waiting for action on the channels.
By having the two channels, we have programmed the logic to walk the filesystem every time we see a change and every 5 minutes, but, crucially, never more than once at a time. In reality, the change watcher is very reliable, and the email generally arrives seconds after the paper comes out of the scanner.
Here is the waitForChange function, which just calls inotifywait. This in turn runs until someone writes a file and then exits. At this point, our function writes 0 into the channel, which kicks the main thread into action. Meanwhile this thread calls inotifywait again, to begin waiting for the next change.
funcsendFile(pathstring,infoos.FileInfo,errerror)error{iferr!=nil{returnerr}ifinfo.IsDir(){returnnil}to,err:=getToAddress(path)iferr!=nil{returnerr}isOpen,err:=fileIsOpen(path)iferr!=nil{returnerr}ifisOpen{returnfmt.Errorf("Skipping %v because it is opened by another process\n",path)}iferr:=sendEmail(to,path);err!=nil{returnerr}iferr:=os.Remove(path);err!=nil{returnerr}returnnil}
This sendFile function is called on every file in the directory tree. This is where Go gets annoyingly verbose. So much error handling! But fairly straightforward. As we walk the tree, we skip directories, send out emails if we have a file, and then delete the file after we send it.
funcsendEmail(tostring,docstring)error{msg:=gomail.NewMessage()msg.SetHeader("From","scanner@xxxx")msg.SetHeader("To",to)msg.SetHeader("Subject","Here is your scanned document")msg.SetBody("text/plain","")msg.Attach(doc)n:=gomail.NewDialer("smtp.example.com",465,"user","pass")iferr:=n.DialAndSend(msg);err!=nil{returnerr}fmt.Printf("Sent %v to %v\n",doc,to)returnnil}
It is relatively simple to send an email using this third-party gomail package. And it isn’t malformed like the scanner’s attempts to send email!
12
13
14
15
16
17
18
19
20
21
22
23
24
funcgetToAddress(pathstring)(string,error){varaddresses=map[string]string{"amy":"xxx@yyyy","nathan":"xxx@wwww",}_,lastDirName:=filepath.Split(filepath.Dir(path))to,ok:=addresses[lastDirName]if!ok{return"",fmt.Errorf("No email address available for %v",lastDirName)}returnto,nil}
This is a relatively simple function that decides who to email to based on the folder that the file is in. This is the abbreviated version; my older kids also have emails configured here.
I bought a new computer. The Beelink U59 Mini PC 11th Gen 4-Cores
N5105 cost me $120 from Amazon. It has a somewhat
recent Intel Processor, 8GB RAM and a 500 GB SSD. It came with Windows
installed, but I erased it and installed Debian with no GUI. It came with
instructions to configure autoboot on power failure so I can hide it away in a
closet.
This fills a gap I’ve had for some time. My house has been laptop-only for
several years now, which leaves me nowhere to run always-on automation things.
I have a Synology that I use for storage, but it is very low powered and
somewhat hard to configure. My blog is served from a Linode virtual server,
which can also fill this gap, but it is also resource-limited, less secure, and
far from most of my data.
It has been fun to play with. I migrated some things from Linode to my
home server and downsized the Linode. I set up a reverse proxy for my printer
and scanner so that I can access the admin pages from outside of my house. I
set up a Gitea server for some of my personal
things. We will see what other useful purposes it can serve.
On Sunday, I was reading a page on Wikipedia, when this quote caught
my attention:
The Gregorian leap cycle, which has 97 leap days spread across 400 years,
contains a whole number of weeks (20871).
This is somewhat surprising to me, since I assume that this was by
accident and not by design, which means there was only a 1-in-7 chance
of it happening.
One result of this is that the calendar for 2000 is the same as the calendar
for 2400, which makes a perpetual calendar such as this one
quite a bit easier to specify.
Another consequence is that not every calendar is equally likely, which
I captured in this silly Mastodon post:
Did you know that October 8 is 3.5% less likely to fall on a Sunday than a
Saturday? Enjoy your rare day!
I have few followers, but I was pretty sure my old internet pal Dr. Drang would
take the bait (he did).
If I were more patient, I could have waited until next February 29, which falls
on a Thursday. Since leap days are more rare than non-leap days, the disparity
is greater, and my 3.5% could have been 14%. Wednesday is the more common day
for February 29, which again reminds us that we are currently on one of the
less common paths through the perpetual calendar.
But calendars are fun to think about, so I didn’t stop there.
A year is about 365.24219 days
long. The Julian calendar has a leap year every 4 years, for an average year
length of 365.25 days. The error, of course, adds up relatively quickly,
as we eventually noticed.
The Gregorian calendar skips leap years on years divisible by 100, unless they
are also divisible by 400, leaving the 97 leap years per 400 years quoted
above. This makes a year on average 365 + 97/400 = 365.2425 days long, which is
closer! It takes 3,225 years before you drift a day, which I guess is good
enough?
At this point, I stumbled upon the Revised Julian calendar,
which skips leap years on years divisible by 100, unless they are also
either 200 or 600 mod 900. This makes a year 365 + 218/900 = 365.24222 days long,
which is even better. Now it takes over 30,000 years before you drift a day.
The rule is much more confusing, though, although it has the benefit (by
design) that it matches the Gregorian calendar exactly for the years 1600-2799.
This lets you claim you are following a more accurate calendar without really
making a fuss. Also, 900 years of the Revised Julian calendar is not a whole
number of weeks, so the Revised Julian perpetual calendar would actually have a
6,300-year cycle.
Finally, I spent some time thinking about what I would have done to handle the
leap year problem if I ran the world. The answer is so obvious that it makes
you doubt the wisdom of our ancestors. The Julian calendar drifts by 1 day
every 128 years. We could have had a calendar (could I call it the Griggorian?)
that had leap years every year divisible by 4
except those also divisible by 128.
This gets you a year with an average length of 365 + 31/128 = 365.2421875
days, which would mean one day of drift every 400,000 years.
I admit that computing divisibility by 128 is harder (for a human) than 400, but
otherwise the rule is clearly simpler. The other downside is that 128 years of
this calendar is not a whole number of weeks, which means that the perpetual
calendar would have a 896-year cycle. But as long as I’m in charge, we might as
well solve that by starting every year on a Sunday.
Balcony on a foggy morning
The Bloc, DTLA
Sihlberg Castle, Zurich
Leg
Hermosa Beach pier
Yard at the end of the day
Foggy morning at the UCSB Lagoon, Isla Vista, California
Lagoon at UCSB campus, Isla Vista
South Park, Hermosa Beach, California
Some kind of mushroom on the edge of a log in Jedediah Smith State Park