initial version; nixos module untested
This commit is contained in:
commit
10cf66ae53
9 changed files with 325 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/gomod2nix-template
|
||||||
95
birdtown-visit-counter.go
Normal file
95
birdtown-visit-counter.go
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type visitsStore struct {
|
||||||
|
m map[string]int
|
||||||
|
*sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *visitsStore) visit(ending string) int {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
oldVisits, has := s.m[ending]
|
||||||
|
var newVisits int
|
||||||
|
if has {
|
||||||
|
newVisits = oldVisits + 1
|
||||||
|
} else {
|
||||||
|
newVisits = 1
|
||||||
|
}
|
||||||
|
s.m[ending] = newVisits
|
||||||
|
return newVisits
|
||||||
|
}
|
||||||
|
|
||||||
|
type visitsHandler struct {
|
||||||
|
store *visitsStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *visitsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// CORS: this is expected to be called from a different domain,
|
||||||
|
// hoops require jumping
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||||
|
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var s string
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("cannot parse data"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(([]byte)(strconv.Itoa(h.store.visit(s))))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
jsonBytes, err := json.Marshal(h.store.m)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("cannot serialize"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(jsonBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Allow", "POST, GET, OPTIONS")
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
addr := flag.String("addr", ":11568", "Listen address")
|
||||||
|
initialFile := flag.String("file", "visits.json", "File with initial data in JSON format")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
initialVisits := make(map[string]int)
|
||||||
|
initialVisitsJson, err := os.ReadFile(*initialFile)
|
||||||
|
if err == nil {
|
||||||
|
json.Unmarshal(initialVisitsJson, &initialVisits)
|
||||||
|
} else {
|
||||||
|
log.Println("initial file", *initialFile, "could not be read, defaulting to empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
store := visitsStore{m: initialVisits, RWMutex: &sync.RWMutex{}}
|
||||||
|
visitsH := &visitsHandler{store: &store}
|
||||||
|
http.Handle("/visits", visitsH)
|
||||||
|
|
||||||
|
log.Println("Starting web server...")
|
||||||
|
http.ListenAndServe(*addr, nil)
|
||||||
|
}
|
||||||
21
default.nix
Normal file
21
default.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{ pkgs ? (
|
||||||
|
let
|
||||||
|
inherit (builtins) fetchTree fromJSON readFile;
|
||||||
|
inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix;
|
||||||
|
in
|
||||||
|
import (fetchTree nixpkgs.locked) {
|
||||||
|
overlays = [
|
||||||
|
(import "${fetchTree gomod2nix.locked}/overlay.nix")
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
, buildGoApplication ? pkgs.buildGoApplication
|
||||||
|
}:
|
||||||
|
|
||||||
|
buildGoApplication {
|
||||||
|
pname = "birdtown-visit-counter";
|
||||||
|
version = "0.1";
|
||||||
|
pwd = ./.;
|
||||||
|
src = ./.;
|
||||||
|
modules = ./gomod2nix.toml;
|
||||||
|
}
|
||||||
85
flake.lock
generated
Normal file
85
flake.lock
generated
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gomod2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1725515722,
|
||||||
|
"narHash": "sha256-+gljgHaflZhQXtr3WjJrGn8NXv7MruVPAORSufuCFnw=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"rev": "1c6fd4e862bf2f249c9114ad625c64c6c29a8a08",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1725983898,
|
||||||
|
"narHash": "sha256-4b3A9zPpxAxLnkF9MawJNHDtOOl6ruL0r6Og1TEDGCE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "1355a0cbfeac61d785b7183c0caaec1f97361b43",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
27
flake.nix
Normal file
27
flake.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
description = "birdtown-visit-counter";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
inputs.gomod2nix.url = "github:nix-community/gomod2nix";
|
||||||
|
inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.gomod2nix.inputs.flake-utils.follows = "flake-utils";
|
||||||
|
|
||||||
|
outputs = inputs @ { self, nixpkgs, flake-utils, gomod2nix }:
|
||||||
|
(flake-utils.lib.eachDefaultSystem
|
||||||
|
(system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
callPackage = pkgs.darwin.apple_sdk_11_0.callPackage or pkgs.callPackage;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.default = callPackage ./. {
|
||||||
|
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
|
||||||
|
};
|
||||||
|
devShells.default = callPackage ./shell.nix {
|
||||||
|
inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix;
|
||||||
|
};
|
||||||
|
nixosModules.default = import ./module.nix inputs;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module git.akho.name/akho/birdtown-visit-counter
|
||||||
|
|
||||||
|
go 1.22
|
||||||
3
gomod2nix.toml
Normal file
3
gomod2nix.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
schema = 3
|
||||||
|
|
||||||
|
[mod]
|
||||||
64
module.nix
Normal file
64
module.nix
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
inputs:
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
inherit (pkgs.stdenv.hostPlatform) system;
|
||||||
|
cfg = config.services.birdtown-visit-counter;
|
||||||
|
inherit (lib) types mkOption mkIf;
|
||||||
|
package = inputs.self.packages."${system}".birdtown-visit-counter;
|
||||||
|
in {
|
||||||
|
options.services.birdtown-visit-counter = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
listenPort = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 11567;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
users.users.birdtown-visit-counter = {
|
||||||
|
group = "birdtown-visit-counter";
|
||||||
|
home = "/var/lib/birdtown-visit-counter";
|
||||||
|
isSystemUser = true;
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
users.groups.birdtown-visit-counter = { };
|
||||||
|
|
||||||
|
systemd.services.birdtown-visit-counter = {
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${package}/bin/birdtown-visit-counter -addr :${cfg.listenPort} -file visits.json";
|
||||||
|
User = "birdtown-visit-counter";
|
||||||
|
Group = "birdtown-visit-counter";
|
||||||
|
WorkingDirectory = "/var/lib/birdtown-visit-counter";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.birdtown-visits-saver = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = "10m";
|
||||||
|
OnUnitActiveSec = "10m";
|
||||||
|
Unit = "birdtown-visits-saver.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.birdtown-visits-saver = {
|
||||||
|
script = ''
|
||||||
|
set -eu
|
||||||
|
if ${pkgs.curl}/bin/curl http://localhost:${cfg.listenPort}/visits > visits-new.json; then
|
||||||
|
cp visits-new.json "$(date +"%Y-%m-%d-%H-%M").json"
|
||||||
|
mv visits-new.json visits.json
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "birdtown-visit-counter";
|
||||||
|
Group = "birdtown-visit-counter";
|
||||||
|
WorkingDirectory = "/var/lib/birdtown-visit-counter";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
26
shell.nix
Normal file
26
shell.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{ pkgs ? (
|
||||||
|
let
|
||||||
|
inherit (builtins) fetchTree fromJSON readFile;
|
||||||
|
inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix;
|
||||||
|
in
|
||||||
|
import (fetchTree nixpkgs.locked) {
|
||||||
|
overlays = [
|
||||||
|
(import "${fetchTree gomod2nix.locked}/overlay.nix")
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
, mkGoEnv ? pkgs.mkGoEnv
|
||||||
|
, gomod2nix ? pkgs.gomod2nix
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
goEnv = mkGoEnv { pwd = ./.; };
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
goEnv
|
||||||
|
pkgs.go-tools
|
||||||
|
pkgs.gopls
|
||||||
|
gomod2nix
|
||||||
|
];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue