Created
July 26, 2020 12:48
-
-
Save ben0x539/9cf66dd8347c264179a89944278a3e61 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# GPLv3 or at yr choice any later | |
require 'fileutils' | |
require 'json' | |
require 'open3' | |
require 'pp' | |
require 'time' | |
require 'uri' | |
class OnePasswordError < StandardError; | |
end | |
def write(name, content) | |
tmp = "#{name}.tmp" | |
File.write(tmp, content) | |
File.rename(tmp, name) | |
end | |
def op(*args) | |
out, err, status = Open3.capture3(["op", "op"], *args) | |
unless status.success? | |
msg = "error: op #{args.join(" ")}; exit status #{status.to_i}" | |
msg << "; output: #{out.chomp}" unless out.empty? | |
msg << "; error: #{err.chomp}" unless err.empty? | |
raise OnePasswordError, msg | |
end | |
STDERR.puts err unless err.empty? | |
out | |
end | |
def signin | |
return if logged_in? | |
out_r, out_w = IO.pipe | |
child = spawn("op", "signin", { out: out_w }) | |
out_w.close | |
out = out_r.read | |
_, status = Process.wait2(child) | |
unless status.success? | |
msg = "error: op signin; exit status #{status.to_i}" | |
msg << "; output: #{out.chomp}" unless out.empty? | |
raise OnePasswordError, msg | |
end | |
m = /^export (OP_SESSION_\w+)="(.*?)"$/.match(out) | |
raise OnePasswordError, "couldn't parse op signin output: #{out.chomp}" \ | |
unless m | |
assignment, k, v = *m | |
ENV[k] = v | |
STDERR.puts "op signin...\n#{assignment}" | |
nil | |
end | |
def logged_in? | |
op("list", "users", { err: "/dev/null" }) | |
true | |
rescue OnePasswordError => e | |
false | |
end | |
def each_in_parallel(vals, &block) | |
queue = Queue.new | |
threads = (0..10).map do | |
Thread.new do | |
while v = queue.pop | |
begin | |
block.call(v) | |
rescue => e | |
STDERR.puts "unhandled error: #{e.full_message}" | |
end | |
end | |
end | |
end | |
vals.each do |v| queue << v end | |
queue.close | |
threads.map{|t|t.join} | |
nil | |
end | |
def need(name, entry) | |
return true unless File.exist?(name) | |
local_date = File.stat(name).mtime | |
remote_date = Time.parse(entry["updatedAt"]) | |
local_date < remote_date | |
end | |
def label(entry) | |
return [] unless overview = entry["overview"] | |
fields = [] | |
if website = overview["url"] | |
parsed = URI.parse(website) rescue nil | |
if parsed && parsed.host | |
website = parsed.host.gsub(/^www\./, "") | |
else | |
website.gsub!(/^https?:\/\/(?:www\.)?/, "") | |
website.gsub!(/\/.*/, "") | |
end | |
fields << website | |
end | |
# logins have a user name | |
if entry["templateUuid"] == "001" && ainfo = overview["ainfo"] | |
fields << ainfo | |
end | |
fields << overview["title"] | |
fields = fields.select{|v| v && !v.empty? && v != "-"}.uniq | |
end | |
def store(kind) | |
plural = "#{kind}s" | |
list = op("list", plural) | |
STDERR.puts "saving #{plural}.json" | |
write("#{plural}.json", list) | |
list = JSON.parse(list) | |
FileUtils.mkdir_p(plural) | |
each_in_parallel(list) do |entry| | |
begin | |
uuid = entry["uuid"] | |
name = "#{plural}/#{uuid}" | |
next unless need(name, entry) | |
content = op("get", kind, uuid) | |
msg = "saving #{name})" | |
detail = label(entry) | |
msg << " (#{detail.join(", ")})" if detail.length > 0 | |
STDERR.puts msg | |
tmp = "#{name}.tmp" | |
write(name, content) | |
rescue => e | |
STDERR.puts "error fetching #{v["uuid"] || "<uuid missing>"}: #{e.full_message}" | |
end | |
end | |
link(plural) | |
end | |
def link_name(entry) | |
link = label(entry).join("_") | |
link &&= link.gsub(/[ _\/]+/, "_").gsub(/^_+|_+$/, "") | |
return nil if link.split.all?{|c| c == '_'} | |
link | |
end | |
def link(plural) | |
list = JSON.parse(File.read("#{plural}.json")) | |
just_checked = {} | |
list.sort_by{|entry| entry["createdAt"]}.each do |entry| | |
next unless link = link_name(entry) | |
link = "#{plural}/#{link}" | |
next if just_checked[link] | |
just_checked[link] ||= true | |
uuid = entry["uuid"] | |
if File.symlink?(link) | |
next if File.readlink(link) == uuid | |
File.unlink(link) | |
elsif File.exists?(link) | |
# don't name your stuff after uuids wtf | |
next | |
end | |
STDERR.puts "linking #{link} -> #{uuid}" | |
File.symlink(uuid, link) | |
end | |
Dir.foreach(plural) do |file| | |
path = "#{plural}/#{file}" | |
next if just_checked[path] | |
next unless File.symlink?(path) | |
STDERR.puts "deleting old link #{path}" | |
File.unlink(path) | |
end | |
end | |
def go | |
signin | |
store("item") | |
store("document") | |
rescue OnePasswordError => e | |
STDERR.puts e.to_s | |
exit 1 | |
end | |
go |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment