Skip to content

Instantly share code, notes, and snippets.

@ben0x539
Created July 26, 2020 12:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ben0x539/9cf66dd8347c264179a89944278a3e61 to your computer and use it in GitHub Desktop.
Save ben0x539/9cf66dd8347c264179a89944278a3e61 to your computer and use it in GitHub Desktop.
#!/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