@@ -60,41 +60,69 @@ CACHE_DIR = BUILD_DIR.join(".cache")
6060BANLISTS = CACHE_DIR .join(" banlists.txt" )
6161BROKEN_POST_URLS = CACHE_DIR .join(" broken_post_urls.txt" )
6262COMMON_ROOT = Path [" _ohmyvps/alpine/alpine-root" ]
63- MEDIA_BUILD_DIR = BUILD_DIR .join(MEDIA_HOST , " /var/www" , PUBLIC_MEDIA_HOST )
63+
64+ SERVER_MEDIA_DIR = Path [" var/www" ].join(PUBLIC_MEDIA_HOST )
65+ MEDIA_BUILD_DIR = BUILD_DIR .join(MEDIA_HOST , SERVER_MEDIA_DIR )
66+
67+ MIRROR_FROM_TO = [
68+ {MEDIA_HOST , [{MIRROR_HOST , SERVER_MEDIA_DIR }]},
69+ ]
6470
6571USER_AGENT = " Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
6672
6773def main
6874 repo_dir = Path [File .dirname(File .realpath(__FILE__ ))]
6975 Dir .cd(repo_dir)
7076
77+ ps = processes()
78+ check(ps)
79+
7180 config = YAML .parse(File .read(MAIN_SITE_CONFIG ))
7281 if ! ARGV .empty? && (ARGV .includes?(" -h" ) || ARGV .includes?(" --help" ))
7382 usage
7483 elsif ARGV .size == 1 && ARGV [0 ] == " sync"
75- check
84+ check_ssh_hosts(ps)
7685 sync
7786 elsif ARGV .size >= 1 && ARGV [0 ] == " sync-nostr"
7887 profiles = ARGV .size > 1 && ARGV [1 ] == " profiles"
7988 sync_nostr(config, profiles: profiles, output_relays: [" ws://localhost:7777" , " wss://nostr.codonaft.com" , " wss://purplepag.es" , " wss://nostr.oxtr.dev" , " wss://nostr.girino.org" ])
8089 elsif ARGV .size == 1 && ARGV [0 ] == " health"
81- check
90+ check_ssh_hosts(ps)
8291 health(config)
8392 elsif ARGV .size >= 1 && ARGV [0 ] == " build"
84- check
8593 update
8694 build
8795 if ARGV .size == 2 && ARGV [1 ] == " run"
8896 serve(DEBUG_HOST )
8997 end
9098 elsif ARGV .size > 2 && ARGV [0 ] == " encode"
91- check
9299 encode_media(ARGV [1 ], config, ARGV [2 ])
93100 elsif ARGV .size == 1 && ARGV [0 ] == " deploy"
94- check
101+ check_ssh_hosts(ps)
95102 deploy(config)
103+ elsif ARGV .size == 1 && ARGV [0 ] == " cloudflare-banlist"
104+ expr_begin = " ip.src in {"
105+ expr_end = " }"
106+ max_len = 4096 - expr_begin.size - expr_end.size
107+ ips = Dir
108+ .glob(" .build/*/var/tmp/local-banlist.txt" )
109+ .flat_map { |i | File .read_lines(i) }
110+ .map { |i | i.strip }
111+ .select { |i | i.size > 0 }
112+ .sort
113+ .uniq
114+ .join(' ' )
115+ while ips.size > 0
116+ end_index = ips.rindex(' ' , max_len)
117+ if end_index.nil? || ips.size <= max_len
118+ puts(" #{ expr_begin } #{ ips.strip } #{ expr_end } \n\n " )
119+ break
120+ else
121+ puts(" #{ expr_begin } #{ ips[0 ..end_index].strip } #{ expr_end } \n\n " )
122+ ips = ips[end_index..].strip
123+ end
124+ end
96125 elsif ARGV .empty? || ARGV [0 ] == " debug"
97- check
98126 update
99127 build
100128 serve(DEBUG_HOST )
@@ -136,6 +164,9 @@ def usage
136164 #{ script } health
137165 run health checks
138166
167+ #{ script } cloudflare-banlist
168+ generate ban-lists for Security - WAF
169+
139170 #{ script } encode path/to/video-name.mkv <en|ru>
140171 #{ script } encode https://youtu.be/CB9bS46vl04 <en|ru>
141172 encode (or download and transcode) media to HLS, put it to #{ MEDIA_BUILD_DIR } /video-name/
158189
159190def build
160191 step(" build" )
192+
193+ if DEBUG
194+ warn(" DEBUG\n " )
195+ else
196+ ok(" PROD\n " )
197+ end
198+
161199 Dir .mkdir_p(CACHE_DIR , 0o755 )
162200
163201 build_vidstack_player
@@ -317,6 +355,10 @@ def sync
317355 hosts = all_hosts()
318356 puts(" hosts: #{ hosts } " )
319357
358+ mirror = MIRROR_FROM_TO .map { |source_host , destination_and_files |
359+ {source_host, destination_and_files.select { |destination_host , _ | hosts.includes?(destination_host) }}
360+ }.to_h
361+
320362 common_files = git_ls(COMMON_ROOT )
321363 hosts_files : Hash (String , Set (Path )) = hosts.map { |host |
322364 host_dir = HOSTS_DIR .join(host)
@@ -327,7 +369,7 @@ def sync
327369 hosts.map { |host |
328370 done = Channel (Nil ).new
329371 spawn do
330- sync_host(host, hosts_files, common_files)
372+ sync_host(host, hosts_files: hosts_files , common_files: common_files, mirror_to: mirror[host]? )
331373 done.send(nil )
332374 end
333375 done
@@ -336,7 +378,7 @@ def sync
336378 ok(" sync finished\n " )
337379end
338380
339- def sync_host (host : String , hosts_files : Hash (String , Set (Path )), common_files : Set (Path ))
381+ def sync_host (host : String , * , hosts_files : Hash (String , Set (Path )), common_files : Set (Path ), mirror_to : Array ({ String , Path }) | Nil )
340382 host_files : Set (Path ) = hosts_files[host]
341383 all_hosts_files : Set (Path ) = hosts_files
342384 .flat_map { |_ , files | files.to_a }
@@ -352,6 +394,15 @@ def sync_host(host : String, hosts_files : Hash(String, Set(Path)), common_files
352394 host_build_files = children_recursive(host_build_dir).to_set
353395 filtered_sync(host, host_build_dir, upload: host_build_files) # TODO: with --delete specifically for jekyll ?
354396
397+ unless mirror_to.nil?
398+ mirror_to.each { |destination_host , dir |
399+ mirror_host_files = host_files.select { |i | i.to_s.starts_with?(dir.to_s) }
400+ mirror_host_build_files = host_build_files.select { |i | i.to_s.starts_with?(dir.to_s) }
401+ filtered_sync(destination_host, HOSTS_DIR .join(host), upload: mirror_host_files.to_set)
402+ filtered_sync(destination_host, BUILD_DIR .join(host), upload: mirror_host_build_files.to_set)
403+ }
404+ end
405+
355406 ssh(host, [" sudo etckeeper commit sync 2>>/dev/null" ])
356407end
357408
@@ -724,41 +775,23 @@ def update_broken_post_urls
724775 broken_urls
725776end
726777
727- def check
728- step(" check" )
729-
730- ps = processes()
778+ def check (ps : Array (Tuple (Int64 , String )))
731779 current_ps_name = " /#{ Path [__FILE__ ].basename} "
732780 raise " other instance keeps running" if ps
733781 .select { |(_ , i )| i.includes?(" crystal" ) && i.includes?(current_ps_name) }
734782 .any? { |(pid , _ )| pid != Process .ppid }
735783
736- puts(" checking dependencies" )
737- system(" which bsdtar bundle css-minify node pnpm podman rsync scour svgcleaner svgo uglifyjs wget >>/dev/null" )
738- raise " missing deps, run: cd && npm install 'css-minify@2.0.0' 'svgo@3.3.2' && USE='lz4 xxhash zstd' emerge '=app-arch/libarchive-3.7.9' '=app-arch/unzip-6.0_p27-r1' '=app-containers/podman-5.3.2' '=dev-ruby/bundler-2.4.22' '=dev-lang/crystal-1.16.1' '=dev-util/uglifyjs-3.16.1' '=media-gfx/scour-0.38.2-r1' '=media-sound/opus-tools-0.2-r1' '=media-libs/libwebp-1.4.0' '=media-video/mediainfo-24.11' '=net-dns/bind-tools-9.18.0-r1' '=net-libs/nodejs-22.13.1' '=net-misc/rsync-3.4.1' '=net-misc/wget-1.25.0' '=sys-apps/pnpm-bin-9.6.0' && cargo install --locked 'svgcleaner@0.9.5' 'websocat@1.13.0'" unless $? .success?
784+ system(" which bsdtar bundle css-minify git nak node pnpm podman rsync scour svgcleaner svgo uglifyjs wget >>/dev/null" )
785+ raise " missing deps, run: cd && npm install 'css-minify@2.0.0' 'svgo@3.3.2' && USE='lz4 xxhash zstd' emerge '=app-arch/libarchive-3.7.9' '=app-arch/unzip-6.0_p27-r1' '=app-containers/podman-5.3.2' '=dev-ruby/bundler-2.4.22' '=dev-lang/crystal-1.16.1' '=dev-util/uglifyjs-3.16.1' '=dev-vcs/git-2.49.0-r2' '=media-gfx/scour-0.38.2-r1' '=media-sound/opus-tools-0.2-r1' '=media-libs/libwebp-1.4.0' '=media-video/mediainfo-24.11' '=net-dns/bind-tools-9.18.0-r1' '=net-libs/nodejs-22.13.1' '=net-misc/rsync-3.4.1' '=net-misc/wget-1.25.0' '=sys-apps/pnpm-bin-9.6.0' && cargo install --locked 'svgcleaner@0.9.5' 'websocat@1.13.0' && go install github.com/fiatjaf/nak@latest" unless $? .success?
739786 system(" podman ps >>/dev/null" )
740787 raise " podman failed" unless $? .success?
741788
742- system(<<-STRING
743- set -euo pipefail
744- #{ TRACE ? " set -x" : " " }
745- for i in $(grep --recursive vim:nofixendofline _includes | cut -d ':' -f1) ; do
746- if [[ $(wc --lines "$i" | awk '{print $1}') -ne 0 ]] ; then
747- echo "unexpected newline in $i"
748- exit 1
749- fi
750- done
751- STRING
752- )
753- raise " file format failure" unless $? .success?
754-
755- check_ssh_hosts(ps)
756-
757- if DEBUG
758- warn(" DEBUG\n " )
759- else
760- ok(" PROD\n " )
761- end
789+ files_with_unexpected_newlines = ` grep --recursive vim:nofixendofline _includes`
790+ .strip
791+ .split('\n' )
792+ .map { |i | i.split(':' )[0 ] }
793+ .select { |i | File .read(i).includes?('\n' ) }
794+ raise " unexpected newline in #{ files_with_unexpected_newlines } " unless files_with_unexpected_newlines.empty?
762795end
763796
764797def build_rust_app (
@@ -1066,26 +1099,29 @@ end
10661099
10671100def check_i2p_host (host : String )
10681101 puts(" checking i2p configuration at #{ host } " )
1069- private_key = File .read_lines(HOSTS_DIR .join(host).join(" etc/i2pd/tunnels.conf" ))
1070- .select { |i | i.starts_with?(" keys =" ) }
1071- .map { |i | i.split(" = " )[1 ] }[0 ]
10721102 service_dir = Path [" /var/lib/i2pd" ]
1103+ private_keys = File .read_lines(HOSTS_DIR .join(host).join(" etc/i2pd/tunnels.conf" ))
1104+ .select { |i | i.starts_with?(" keys =" ) }
1105+ .map { |i | i.split(" = " )[1 ] }
10731106 check_manual_upload(host, owner: " i2pd" , group: " i2pd" , mode: 700 , path: service_dir)
1074- check_manual_upload(host, owner: " i2pd" , group: " i2pd" , mode: 440 , path: service_dir.join(private_key))
1107+ private_keys.each { | i | check_manual_upload(host, owner: " i2pd" , group: " i2pd" , mode: 440 , path: service_dir.join(i)) }
10751108end
10761109
10771110def check_tor_host (host : String )
10781111 puts(" checking tor configuration at #{ host } " )
1079- service_dir = Path [ File .read_lines(HOSTS_DIR .join(host).join(" etc/tor/torrc" ))
1112+ service_dirs = File .read_lines(HOSTS_DIR .join(host).join(" etc/tor/torrc" ))
10801113 .select { |i | i.starts_with?(" HiddenServiceDir" ) }
1081- .map { |i | i.split(" " )[1 ] }[0 ]]
1082- check_manual_upload(host, owner: " tor" , group: " tor" , mode: 700 , path: service_dir)
1083- check_manual_upload(host, owner: " tor" , group: " tor" , mode: 400 , path: service_dir.join(" hs_ed25519_secret_key" ))
1084- check_manual_upload(host, owner: " tor" , group: " tor" , mode: 400 , path: service_dir.join(" hs_ed25519_public_key" ))
1085- check_manual_upload(host, owner: " tor" , group: " tor" , mode: 400 , path: service_dir.join(" hostname" ), data: service_dir.basename)
1114+ .map { |i | Path [i.split(" " )[1 ]] }
1115+ service_dirs.map { |i |
1116+ check_manual_upload(host, owner: " tor" , group: " tor" , mode: 700 , path: i)
1117+ check_manual_upload(host, owner: " tor" , group: " tor" , mode: 400 , path: i.join(" hs_ed25519_secret_key" ))
1118+ check_manual_upload(host, owner: " tor" , group: " tor" , mode: 400 , path: i.join(" hs_ed25519_public_key" ))
1119+ check_manual_upload(host, owner: " tor" , group: " tor" , mode: 400 , path: i.join(" hostname" ), data: i.basename)
1120+ }
10861121end
10871122
10881123def check_ssh_hosts (ps : Array (Tuple (Int64 , String )))
1124+ step(" check_ssh_hosts" )
10891125 all_hosts()
10901126 .map { |i |
10911127 check_existing_ssh_connection(i, ps)
@@ -1132,12 +1168,15 @@ def check_existing_ssh_connection(host : String, ps : Array(Tuple(Int64, String)
11321168end
11331169
11341170def check_manual_upload (host : String , * , owner : String , group : String , mode : UInt16 , path : Path , data : String ? = nil )
1135- raise " #{ path } : unexpected owner" unless ssh(host, [" sudo stat -c %U #{ path } " ]) == owner
1136- raise " #{ path } : unexpected group" unless ssh(host, [" sudo stat -c %G #{ path } " ]) == group
1137- raise " #{ path } : unexpected mode" unless ssh(host, [" sudo stat -c %a #{ path } " ]).to_i == mode
1138- unless data.nil?
1139- raise " #{ path } : unexpected data" unless ssh(host, [" sudo cat #{ path } " ]).strip == data
1140- end
1171+ commands = [" stat -c %U,%G,%a" ]
1172+ commands += [" cat" ] unless data.nil?
1173+ output = ssh(host, commands.map { |i | " sudo #{ i } #{ path } " }).strip.split('\n' )
1174+ stat = output[0 ].split(',' )
1175+
1176+ raise " #{ path } : unexpected owner" unless stat[0 ] == owner
1177+ raise " #{ path } : unexpected group" unless stat[1 ] == group
1178+ raise " #{ path } : unexpected mode" unless stat[2 ].to_i == mode
1179+ raise " #{ path } : unexpected data" unless data.nil? || output[1 ] == data
11411180end
11421181
11431182def sync_nostr (config, * , profiles : Bool , output_relays : Array (String ))
0 commit comments