Skip to content

Instantly share code, notes, and snippets.

@AugustoPedraza
Created January 16, 2026 02:25
Show Gist options
  • Select an option

  • Save AugustoPedraza/2c6d196356ecd2e1476b9fa70f6b317c to your computer and use it in GitHub Desktop.

Select an option

Save AugustoPedraza/2c6d196356ecd2e1476b9fa70f6b317c to your computer and use it in GitHub Desktop.
Export account data from old Preneur app
# === REMOTE EXPORT & UPLOAD SCRIPT ===
#
# RUN ON OLD SERVER (one line in IEx):
#
# username = "amadeu"; :inets.start(); :ssl.start(); {:ok, {{_, 200, _}, _, body}} = :httpc.request(:get, {~c"https://raw.githubusercontent.com/snapcall-me/life/development/priv/migration/export_and_upload.exs", []}, [], []); Code.eval_string(to_string(body), [username: username])
#
# This will:
# 1. Export account data to JSON
# 2. Upload to Cloudinary
# 3. Print the download URL for importing on staging
#
# =================================================================
import Ecto.Query
defmodule ExportUpload do
@moduledoc "Export account data and upload to Cloudinary"
# Cloudinary credentials - reads from environment
def cloudinary_config do
%{
cloud_name: System.get_env("CLOUDINARY_CLOUD_NAME"),
api_key: System.get_env("CLOUDINARY_API_KEY"),
api_secret: System.get_env("CLOUDINARY_SECRET")
}
end
def to_map(nil), do: nil
def to_map(%Ecto.Association.NotLoaded{}), do: nil
def to_map(%{__struct__: _} = struct) do
struct
|> Map.from_struct()
|> Map.drop([:__meta__, :__struct__])
|> Enum.map(fn {k, v} -> {k, to_map(v)} end)
|> Map.new()
end
def to_map(list) when is_list(list), do: Enum.map(list, &to_map/1)
def to_map(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> Enum.map(&to_map/1)
def to_map(%{} = map), do: map |> Enum.map(fn {k, v} -> {k, to_map(v)} end) |> Map.new()
def to_map(other), do: other
def export(username) do
IO.puts("Exporting #{username}...")
account = from(a in Accounts.Account, where: a.username == ^username, preload: [:user])
|> Accounts.Repo.one()
if is_nil(account) do
raise "Account not found: #{username}"
end
contacts_ctx = from(c in Contacts.Context, where: c.snapcall_acc_id == ^account.id, limit: 1)
|> Contacts.Repo.one()
comm_ctx = from(c in Comm.Context, where: c.snapcall_acc_id == ^account.id, limit: 1)
|> Comm.Repo.one()
seg_ctx = from(c in Segmentation.Context, where: c.snapcall_acc_id == ^account.id, limit: 1)
|> Segmentation.Repo.one()
# Export contacts with phone numbers
contacts = if contacts_ctx do
raw = Contacts.Repo.preload(contacts_ctx, :contacts).contacts
|> Enum.filter(fn c -> is_nil(c.deleted) or c.deleted == false end)
ids = Enum.map(raw, & &1.id)
phones = from(pn in Contacts.PhoneNumber, where: pn.contact_id in ^ids)
|> Contacts.Repo.all()
|> Enum.group_by(& &1.contact_id)
Enum.map(raw, fn c -> Map.put(c, :phone_numbers, Map.get(phones, c.id, [])) end)
else
[]
end
# Export Twilio config
twilio = if comm_ctx do
from(pn in Comm.TwilioPhoneNumber, where: pn.context_id == ^comm_ctx.id, preload: [:twilio_account])
|> Comm.Repo.one()
end
# Export keywords
keywords = if comm_ctx do
from(k in Comm.SmsAutoReply, where: k.context_id == ^comm_ctx.id) |> Comm.Repo.all()
else
[]
end
# Export groups
groups = if seg_ctx do
from(g in Segmentation.Group,
where: g.context_id == ^seg_ctx.id and (is_nil(g.deleted) or g.deleted == false))
|> Segmentation.Repo.all()
else
[]
end
# Export group memberships
group_memberships = if contacts_ctx do
from(sg in Contacts.SegmentationGrouping,
where: sg.context_id == ^contacts_ctx.id,
select: %{contact_id: sg.contact_id, group_id: sg.group_id})
|> Contacts.Repo.all()
else
[]
end
# Merge group IDs into contacts
contacts_with_groups = Enum.map(contacts, fn contact ->
group_ids = group_memberships
|> Enum.filter(fn m -> m.contact_id == contact.id end)
|> Enum.map(fn m -> m.group_id end)
contact |> to_map() |> Map.put(:group_ids, group_ids)
end)
# Build export data
export_data = %{
account: to_map(account),
contacts: contacts_with_groups,
twilio: if(twilio, do: %{
phone_number: twilio.phone_number,
phone_sid: twilio.sid,
twilio_account_sid: twilio.twilio_account.sid,
twilio_auth_token: twilio.twilio_account.auth_token
}),
keywords: Enum.map(keywords, &to_map/1),
groups: Enum.map(groups, fn g -> %{id: g.id, name: g.name, color: g.color} end)
}
json = Jason.encode!(export_data, pretty: true)
IO.puts("=== EXPORT COMPLETE ===")
IO.puts("Account: #{username}")
IO.puts("Contacts: #{length(contacts)}")
IO.puts("Groups: #{length(groups)}")
IO.puts("Twilio: #{if twilio, do: "Yes", else: "No"}")
IO.puts("Keywords: #{length(keywords)}")
{username, json}
end
def upload_to_cloudinary(username, json) do
config = cloudinary_config()
if is_nil(config.cloud_name) or is_nil(config.api_key) or is_nil(config.api_secret) do
# Save locally if no Cloudinary config
filename = "/tmp/#{username}_export.json"
File.write!(filename, json)
IO.puts("\nNo Cloudinary config found. Saved to: #{filename}")
{:local, filename}
else
IO.puts("\nUploading to Cloudinary...")
timestamp = System.system_time(:second) |> to_string()
public_id = "migration/#{username}"
# Create signature
sign_string = "folder=migration&public_id=#{username}&timestamp=#{timestamp}#{config.api_secret}"
signature = :crypto.hash(:sha, sign_string) |> Base.encode16(case: :lower)
# Build multipart form
boundary = "----ExportBoundary#{:rand.uniform(999999999)}"
body = build_multipart_body(boundary, [
{"file", json, "#{username}_export.json"},
{"folder", "migration"},
{"public_id", username},
{"timestamp", timestamp},
{"api_key", config.api_key},
{"signature", signature}
])
url = ~c"https://api.cloudinary.com/v1_1/#{config.cloud_name}/raw/upload"
content_type = ~c"multipart/form-data; boundary=#{boundary}"
case :httpc.request(:post, {url, [], content_type, body}, [], []) do
{:ok, {{_, 200, _}, _, response_body}} ->
response = Jason.decode!(to_string(response_body))
secure_url = response["secure_url"]
IO.puts("=== UPLOAD COMPLETE ===")
IO.puts("URL: #{secure_url}")
IO.puts("\n=== IMPORT ON STAGING ===")
IO.puts("curl -o /tmp/#{username}_export.json \"#{secure_url}\"")
IO.puts("Life.Migration.JsonImporter.import_from_file(\"/tmp/#{username}_export.json\")")
{:ok, secure_url}
{:ok, {{_, status, _}, _, response_body}} ->
IO.puts("Upload failed (#{status}): #{response_body}")
# Fallback to local
filename = "/tmp/#{username}_export.json"
File.write!(filename, json)
IO.puts("Saved locally: #{filename}")
{:error, status}
{:error, reason} ->
IO.puts("Upload error: #{inspect(reason)}")
filename = "/tmp/#{username}_export.json"
File.write!(filename, json)
IO.puts("Saved locally: #{filename}")
{:error, reason}
end
end
end
defp build_multipart_body(boundary, parts) do
parts_binary = Enum.map(parts, fn
{name, content, filename} ->
"""
--#{boundary}\r
Content-Disposition: form-data; name="#{name}"; filename="#{filename}"\r
Content-Type: application/json\r
\r
#{content}\r
"""
{name, value} ->
"""
--#{boundary}\r
Content-Disposition: form-data; name="#{name}"\r
\r
#{value}\r
"""
end)
|> Enum.join("")
parts_binary <> "--#{boundary}--\r\n"
end
def run(username) do
{username, json} = export(username)
upload_to_cloudinary(username, json)
end
end
# Run the export
ExportUpload.run(username)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment