initial version; nixos module untested

This commit is contained in:
Alexander Khodyrev 2024-09-12 20:23:15 +03:00
commit 10cf66ae53
9 changed files with 325 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/gomod2nix-template

95
birdtown-visit-counter.go Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
module git.akho.name/akho/birdtown-visit-counter
go 1.22

3
gomod2nix.toml Normal file
View file

@ -0,0 +1,3 @@
schema = 3
[mod]

64
module.nix Normal file
View 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
View 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
];
}