mirror of
https://github.com/System-End/cdn.git
synced 2026-04-19 16:18:17 +00:00
whole buncha stuff
This commit is contained in:
parent
3d8b180b11
commit
5ce2ea55ad
25 changed files with 568 additions and 17 deletions
|
|
@ -5,6 +5,8 @@ class Components::Base < Phlex::HTML
|
|||
register_value_helper :current_user
|
||||
# Include any helpers you want to be available across all components
|
||||
include Phlex::Rails::Helpers::Routes
|
||||
include Phlex::Rails::Helpers::ButtonTo
|
||||
include Phlex::Rails::Helpers::TimeAgoInWords
|
||||
|
||||
if Rails.env.development?
|
||||
def before_template
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class Components::HeaderBar < Components::Base
|
|||
plain current_user.name
|
||||
end
|
||||
|
||||
menu.with_item(label: "Log out", href: logout_path, data: { method: :delete }) do |item|
|
||||
menu.with_item(label: "Log out", href: logout_path, form_arguments: { method: :delete }) do |item|
|
||||
item.with_leading_visual_icon(icon: :"sign-out")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
51
app/components/static_pages/base.rb
Normal file
51
app/components/static_pages/base.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::StaticPages::Base < Components::Base
|
||||
def stat_card(title, value, icon)
|
||||
div(style: "padding: 14px; background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px;") do
|
||||
div(style: "display: flex; justify-content: space-between; align-items: flex-start;") do
|
||||
div do
|
||||
p(style: "font-size: 11px; color: var(--fgColor-muted, #656d76); margin: 0 0 4px; text-transform: uppercase; letter-spacing: 0.3px;") { title }
|
||||
span(style: "font-size: 28px; font-weight: 600; line-height: 1;") { value.to_s }
|
||||
end
|
||||
span(style: "color: var(--fgColor-muted, #656d76);") do
|
||||
render Primer::Beta::Octicon.new(icon: icon, size: :small)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_panel(title, links)
|
||||
div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do
|
||||
div(style: "padding: 12px 16px; border-bottom: 1px solid var(--borderColor-default, #d0d7de); background: var(--bgColor-muted, #f6f8fa);") do
|
||||
h3(style: "font-size: 14px; font-weight: 600; margin: 0;") { title }
|
||||
end
|
||||
div(style: "padding: 8px 0;") do
|
||||
links.each do |link|
|
||||
a(
|
||||
href: link[:href],
|
||||
target: link[:href].start_with?("http") ? "_blank" : nil,
|
||||
rel: link[:href].start_with?("http") ? "noopener" : nil,
|
||||
style: "display: flex; align-items: center; gap: 12px; padding: 10px 16px; text-decoration: none; color: inherit;"
|
||||
) do
|
||||
span(style: "color: var(--fgColor-muted, #656d76);") do
|
||||
render Primer::Beta::Octicon.new(icon: link[:icon], size: :small)
|
||||
end
|
||||
span(style: "font-size: 14px;") { link[:label] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resources_panel
|
||||
links = [
|
||||
{ label: "API Docs", href: "https://github.com/hackclub/cdn#-api-usage", icon: :book },
|
||||
{ label: "GitHub Repo", href: "https://github.com/hackclub/cdn", icon: :"mark-github" },
|
||||
{ label: "Use via Slack", href: "https://hackclub.enterprise.slack.com/archives/C016DEDUL87", icon: :"comment-discussion" },
|
||||
{ label: "Help with development?", href: "https://hackclub.enterprise.slack.com/archives/C0ACGUA6XTJ", icon: :heart },
|
||||
{ label: "Report an Issue", href: "https://github.com/hackclub/cdn/issues", icon: :"issue-opened" }
|
||||
]
|
||||
link_panel("Resources", links)
|
||||
end
|
||||
end
|
||||
91
app/components/static_pages/home.rb
Normal file
91
app/components/static_pages/home.rb
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::StaticPages::Home < Components::StaticPages::Base
|
||||
def initialize(stats:, user:)
|
||||
@stats = stats
|
||||
@user = user
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
|
||||
header_section
|
||||
kpi_section
|
||||
main_section
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :stats, :user
|
||||
|
||||
def header_section
|
||||
header(style: "display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 16px; padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--borderColor-default, #d0d7de);") do
|
||||
div do
|
||||
p(style: "color: var(--fgColor-muted, #656d76); margin: 0 0 4px; font-size: 14px;") do
|
||||
plain "Welcome back, "
|
||||
strong { user&.name || "friend" }
|
||||
end
|
||||
h1(style: "font-size: 2rem; font-weight: 300; margin: 0;") { "Your CDN Stash" }
|
||||
end
|
||||
|
||||
div(style: "display: flex; gap: 8px; flex-wrap: wrap;") do
|
||||
a(href: "https://github.com/hackclub/cdn", target: "_blank", rel: "noopener", class: "btn") do
|
||||
render Primer::Beta::Octicon.new(icon: :"mark-github", mr: 1)
|
||||
plain "View on GitHub"
|
||||
end
|
||||
a(href: "https://app.slack.com/client/T0266FRGM/C016DEDUL87", target: "_blank", rel: "noopener", class: "btn btn-primary") do
|
||||
render Primer::Beta::Octicon.new(icon: :"comment-discussion", mr: 1)
|
||||
plain "Join #cdn"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def kpi_section
|
||||
div(style: "margin-bottom: 32px;") do
|
||||
# Your stats section
|
||||
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 12px;") { "Your Stats" }
|
||||
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px;") do
|
||||
stat_card("Total files", stats[:total_files], :archive)
|
||||
stat_card("Storage used", stats[:storage_formatted], :database)
|
||||
stat_card("Uploaded today", stats[:files_today], :upload)
|
||||
stat_card("This week", stats[:files_this_week], :zap)
|
||||
end
|
||||
|
||||
# Recent uploads
|
||||
if stats[:recent_uploads].any?
|
||||
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 24px 0 12px;") { "Recent Uploads" }
|
||||
recent_uploads_list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def recent_uploads_list
|
||||
div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do
|
||||
stats[:recent_uploads].each_with_index do |upload, index|
|
||||
div(style: "padding: 12px 16px; #{index > 0 ? 'border-top: 1px solid var(--borderColor-default, #d0d7de);' : ''}") do
|
||||
div(style: "display: flex; justify-content: space-between; align-items: center; gap: 16px;") do
|
||||
div(style: "flex: 1; min-width: 0;") do
|
||||
div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do
|
||||
render Primer::Beta::Octicon.new(icon: :file, size: :small, mr: 1)
|
||||
plain upload.filename.to_s
|
||||
end
|
||||
div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin-top: 4px;") do
|
||||
plain "#{upload.human_file_size} • #{time_ago_in_words(upload.created_at)} ago"
|
||||
end
|
||||
end
|
||||
a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do
|
||||
plain "View"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def main_section
|
||||
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;") do
|
||||
resources_panel
|
||||
end
|
||||
end
|
||||
end
|
||||
74
app/components/static_pages/logged_out.rb
Normal file
74
app/components/static_pages/logged_out.rb
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::StaticPages::LoggedOut < Components::StaticPages::Base
|
||||
def initialize(stats:)
|
||||
@stats = stats
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(style: "max-width: 1200px; margin: 0 auto; padding: 48px 24px 24px;") do
|
||||
header_section
|
||||
stats_section
|
||||
main_section
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :stats
|
||||
|
||||
def header_section
|
||||
header(style: "display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 16px; padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--borderColor-default, #d0d7de);") do
|
||||
div do
|
||||
h1(style: "font-size: 2rem; font-weight: 300; margin: 0 0 8px;") do
|
||||
plain "Hack Club CDN"
|
||||
sup(style: "font-size: 0.5em; margin-left: 4px;") { "v4" }
|
||||
end
|
||||
p(style: "color: var(--fgColor-muted, #656d76); margin: 0; max-width: 600px;") do
|
||||
plain "File hosting for Hack Clubbers."
|
||||
end
|
||||
end
|
||||
|
||||
div(style: "display: flex; gap: 8px; flex-wrap: wrap;") do
|
||||
button_to "Sign in with Hack Club", "/auth/hack_club", method: :post, class: "btn btn-primary", data: { turbo: false }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stats_section
|
||||
div(style: "margin-bottom: 32px;") do
|
||||
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 12px;") { "State of the Platform:" }
|
||||
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px;") do
|
||||
stat_card("Total files", stats[:total_files], :archive)
|
||||
stat_card("Storage used", stats[:storage_formatted], :database)
|
||||
stat_card("Users", stats[:total_users], :people)
|
||||
stat_card("Files this week", stats[:files_this_week], :zap)
|
||||
end
|
||||
|
||||
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 24px 0 12px;") { "New in V4:" }
|
||||
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;") do
|
||||
feature_card(:lock, "Invincible", "Backups of the underlying storage exist.")
|
||||
feature_card(:link, "No broken links, this time?", "it lives on a domain! that we own!")
|
||||
feature_card(:"shield-check", "Hopefully reliable", 'Backed by the award-winning "cc @nora" service guarantee.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def feature_card(icon, title, description)
|
||||
div(style: "padding: 14px; background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px;") do
|
||||
div(style: "display: flex; align-items: center; gap: 10px; margin-bottom: 6px;") do
|
||||
span(style: "color: var(--fgColor-muted, #656d76);") do
|
||||
render Primer::Beta::Octicon.new(icon: icon, size: :small)
|
||||
end
|
||||
h3(style: "font-size: 14px; font-weight: 600; margin: 0;") { title }
|
||||
end
|
||||
p(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin: 0;") { description }
|
||||
end
|
||||
end
|
||||
|
||||
def main_section
|
||||
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;") do
|
||||
resources_panel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,7 +13,7 @@ class SessionsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
reset_session
|
||||
redirect_to login_path, notice: "Signed out successfully!"
|
||||
redirect_to root_path, notice: "Signed out successfully!"
|
||||
end
|
||||
|
||||
def failure
|
||||
|
|
|
|||
|
|
@ -2,5 +2,10 @@ class StaticPagesController < ApplicationController
|
|||
skip_before_action :require_authentication!, only: [:home]
|
||||
|
||||
def home
|
||||
if signed_in?
|
||||
@user_stats = CDNStatsService.user_stats(current_user)
|
||||
else
|
||||
@global_stats = CDNStatsService.global_stats
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
import Rails from "@rails/ujs";
|
||||
Rails.start();
|
||||
import "@primer/view-components/app/components/primer/primer.js";
|
||||
|
||||
|
|
|
|||
9
app/jobs/refresh_cdn_stats_job.rb
Normal file
9
app/jobs/refresh_cdn_stats_job.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RefreshCDNStatsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
CDNStatsService.refresh_global_stats!
|
||||
end
|
||||
end
|
||||
|
|
@ -1,3 +1,12 @@
|
|||
class ApplicationRecord < ActiveRecord::Base
|
||||
primary_abstract_class
|
||||
|
||||
before_create :generate_uuid_v7
|
||||
|
||||
private
|
||||
|
||||
def generate_uuid_v7
|
||||
return if self.class.attribute_types['id'].type != :uuid
|
||||
self.id ||= SecureRandom.uuid_v7
|
||||
end
|
||||
end
|
||||
|
|
|
|||
57
app/models/upload.rb
Normal file
57
app/models/upload.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Upload < ApplicationRecord
|
||||
# UUID v7 primary key (automatic via migration)
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :blob, class_name: 'ActiveStorage::Blob'
|
||||
|
||||
# Delegate file metadata to blob (no duplication!)
|
||||
delegate :filename, :byte_size, :content_type, :checksum, to: :blob
|
||||
|
||||
# Aliases for consistency
|
||||
alias_method :file_size, :byte_size
|
||||
alias_method :mime_type, :content_type
|
||||
|
||||
# Provenance enum
|
||||
enum :provenance, {
|
||||
slack: 'slack',
|
||||
web: 'web',
|
||||
api: 'api',
|
||||
rescued: 'rescued'
|
||||
}, validate: true
|
||||
|
||||
validates :provenance, presence: true
|
||||
|
||||
scope :recent, -> { order(created_at: :desc) }
|
||||
scope :by_user, ->(user) { where(user: user) }
|
||||
scope :today, -> { where('created_at >= ?', Time.zone.now.beginning_of_day) }
|
||||
scope :this_week, -> { where('created_at >= ?', Time.zone.now.beginning_of_week) }
|
||||
scope :this_month, -> { where('created_at >= ?', Time.zone.now.beginning_of_month) }
|
||||
|
||||
def human_file_size
|
||||
ActiveSupport::NumberHelper.number_to_human_size(byte_size)
|
||||
end
|
||||
|
||||
# Get CDN URL (generated from blob)
|
||||
def cdn_url
|
||||
Rails.application.routes.url_helpers.rails_blob_url(blob, host: ENV['CDN_HOST'] || 'cdn.hackclub.com')
|
||||
end
|
||||
|
||||
# Create upload from URL (for API/rescue operations)
|
||||
def self.create_from_url(url, user:, provenance:, original_url: nil)
|
||||
downloaded = URI.open(url)
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: downloaded,
|
||||
filename: File.basename(URI.parse(url).path)
|
||||
)
|
||||
|
||||
create!(
|
||||
user: user,
|
||||
blob: blob,
|
||||
provenance: provenance,
|
||||
original_url: original_url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -9,6 +9,8 @@ class User < ApplicationRecord
|
|||
validates :hca_id, presence: true, uniqueness: true
|
||||
encrypts :hca_access_token
|
||||
|
||||
has_many :uploads, dependent: :destroy
|
||||
|
||||
def self.find_or_create_from_omniauth(auth)
|
||||
hca_id = auth.uid
|
||||
slack_id = auth.extra.raw_info.slack_id
|
||||
|
|
@ -38,4 +40,16 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def hca_profile(access_token) = HCAService.new(access_token).me
|
||||
|
||||
def total_files
|
||||
uploads.count
|
||||
end
|
||||
|
||||
def total_storage_bytes
|
||||
uploads.joins(:blob).sum('active_storage_blobs.byte_size')
|
||||
end
|
||||
|
||||
def total_storage_gb
|
||||
(total_storage_bytes / 1.gigabyte.to_f).round(2)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
49
app/services/cdn_stats_service.rb
Normal file
49
app/services/cdn_stats_service.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CDNStatsService
|
||||
CACHE_KEY_GLOBAL = "cdn:stats:global"
|
||||
CACHE_DURATION = 5.minutes
|
||||
|
||||
# Global stats (cached) - for logged-out users
|
||||
def self.global_stats
|
||||
Rails.cache.fetch(CACHE_KEY_GLOBAL, expires_in: CACHE_DURATION) do
|
||||
calculate_global_stats
|
||||
end
|
||||
end
|
||||
|
||||
# Force refresh global stats (called by background job)
|
||||
def self.refresh_global_stats!
|
||||
Rails.cache.delete(CACHE_KEY_GLOBAL)
|
||||
global_stats
|
||||
end
|
||||
|
||||
# User stats (live) - for logged-in users
|
||||
def self.user_stats(user)
|
||||
{
|
||||
total_files: user.total_files,
|
||||
total_storage: user.total_storage_gb,
|
||||
storage_formatted: "#{user.total_storage_gb} GB",
|
||||
files_today: user.uploads.today.count,
|
||||
files_this_week: user.uploads.this_week.count,
|
||||
recent_uploads: user.uploads.includes(:blob).recent.limit(5)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.calculate_global_stats
|
||||
total_files = Upload.count
|
||||
total_storage_bytes = Upload.joins(:blob).sum('active_storage_blobs.byte_size')
|
||||
total_storage_gb = (total_storage_bytes / 1.gigabyte.to_f).round(2)
|
||||
total_users = User.joins(:uploads).distinct.count
|
||||
|
||||
{
|
||||
total_files: total_files,
|
||||
total_storage_gb: total_storage_gb,
|
||||
storage_formatted: "#{total_storage_gb} GB",
|
||||
total_users: total_users,
|
||||
files_today: Upload.today.count,
|
||||
files_this_week: Upload.this_week.count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<%= render(Components::HeaderBar.new) %>
|
||||
<%= render(Components::HeaderBar.new) if signed_in? %>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<% if signed_in? %>
|
||||
|
||||
<%= render Components::StaticPages::Home.new(stats: @user_stats, user: current_user) %>
|
||||
<% else %>
|
||||
<%= button_to "Sign in with Hack Club", "/auth/hack_club", method: :post, class: "btn btn-primary btn-large", data: { turbo: false } %>
|
||||
<% end %>
|
||||
<%= render Components::StaticPages::LoggedOut.new(stats: @global_stats) %>
|
||||
<% end %>
|
||||
|
|
@ -21,8 +21,8 @@ Rails.application.configure do
|
|||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||
# config.asset_host = "http://assets.example.com"
|
||||
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
config.active_storage.service = :local
|
||||
# Store uploaded files on Amazon S3 or S3-compatible storage (see config/storage.yml for options).
|
||||
config.active_storage.service = :amazon
|
||||
|
||||
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
||||
config.assume_ssl = true
|
||||
|
|
|
|||
|
|
@ -13,3 +13,6 @@ production:
|
|||
clear_solid_queue_finished_jobs:
|
||||
command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
|
||||
schedule: every hour at minute 12
|
||||
refresh_cdn_stats:
|
||||
class: RefreshCDNStatsJob
|
||||
schedule: every 5 minutes
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ local:
|
|||
service: Disk
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
|
||||
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
||||
# amazon:
|
||||
# service: S3
|
||||
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
||||
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||
# region: us-east-1
|
||||
# bucket: your_own_bucket-<%= Rails.env %>
|
||||
# Cloudflare R2 (S3-compatible)
|
||||
amazon:
|
||||
service: S3
|
||||
access_key_id: <%= ENV['R2_ACCESS_KEY_ID'] %>
|
||||
secret_access_key: <%= ENV['R2_SECRET_ACCESS_KEY'] %>
|
||||
region: auto
|
||||
bucket: <%= ENV['R2_BUCKET_NAME'] %>
|
||||
endpoint: <%= ENV['R2_ENDPOINT'] %> # Format: https://<account_id>.r2.cloudflarestorage.com
|
||||
force_path_style: true
|
||||
|
||||
# Remember not to checkin your GCS keyfile to a repository
|
||||
# google:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# This migration comes from active_storage (originally 20170806125915)
|
||||
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
# Use Active Record's configured type for primary and foreign keys
|
||||
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
||||
|
||||
create_table :active_storage_blobs, id: primary_key_type do |t|
|
||||
t.string :key, null: false
|
||||
t.string :filename, null: false
|
||||
t.string :content_type
|
||||
t.text :metadata
|
||||
t.string :service_name, null: false
|
||||
t.bigint :byte_size, null: false
|
||||
t.string :checksum
|
||||
|
||||
if connection.supports_datetime_with_precision?
|
||||
t.datetime :created_at, precision: 6, null: false
|
||||
else
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
|
||||
t.index [ :key ], unique: true
|
||||
end
|
||||
|
||||
create_table :active_storage_attachments, id: primary_key_type do |t|
|
||||
t.string :name, null: false
|
||||
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
|
||||
t.references :blob, null: false, type: foreign_key_type
|
||||
|
||||
if connection.supports_datetime_with_precision?
|
||||
t.datetime :created_at, precision: 6, null: false
|
||||
else
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
|
||||
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
|
||||
create_table :active_storage_variant_records, id: primary_key_type do |t|
|
||||
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
|
||||
t.string :variation_digest, null: false
|
||||
|
||||
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def primary_and_foreign_key_types
|
||||
config = Rails.configuration.generators
|
||||
setting = config.options[config.orm][:primary_key_type]
|
||||
primary_key_type = setting || :primary_key
|
||||
foreign_key_type = setting || :bigint
|
||||
[ primary_key_type, foreign_key_type ]
|
||||
end
|
||||
end
|
||||
20
db/migrate/20260129051531_create_uploads.rb
Normal file
20
db/migrate/20260129051531_create_uploads.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
class CreateUploads < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :uploads, id: :uuid do |t|
|
||||
t.references :user, null: false, foreign_key: true, type: :bigint
|
||||
t.references :blob, null: false, foreign_key: { to_table: :active_storage_blobs }
|
||||
|
||||
# Upload source tracking
|
||||
t.string :provenance, null: false # enum: slack, web, api, rescued
|
||||
|
||||
# For rescued files from old hel1 bucket
|
||||
t.string :original_url # Old CDN URL to fixup
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index [:user_id, :created_at]
|
||||
t.index :created_at
|
||||
t.index :provenance
|
||||
end
|
||||
end
|
||||
end
|
||||
49
db/schema.rb
generated
49
db/schema.rb
generated
|
|
@ -10,10 +10,52 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_01_27_174404) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_01_29_051531) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_blobs", force: :cascade do |t|
|
||||
t.string "key", null: false
|
||||
t.string "filename", null: false
|
||||
t.string "content_type"
|
||||
t.text "metadata"
|
||||
t.string "service_name", null: false
|
||||
t.bigint "byte_size", null: false
|
||||
t.string "checksum"
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "uploads", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "provenance", null: false
|
||||
t.string "original_url"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["blob_id"], name: "index_uploads_on_blob_id"
|
||||
t.index ["created_at"], name: "index_uploads_on_created_at"
|
||||
t.index ["provenance"], name: "index_uploads_on_provenance"
|
||||
t.index ["user_id", "created_at"], name: "index_uploads_on_user_id_and_created_at"
|
||||
t.index ["user_id"], name: "index_uploads_on_user_id"
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "hca_id"
|
||||
t.text "hca_access_token"
|
||||
|
|
@ -26,4 +68,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_27_174404) do
|
|||
t.index ["hca_id"], name: "index_users_on_hca_id", unique: true
|
||||
t.index ["slack_id"], name: "index_users_on_slack_id", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "uploads", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "uploads", "users"
|
||||
end
|
||||
|
|
|
|||
38
db/seeds.rb
38
db/seeds.rb
|
|
@ -7,3 +7,41 @@
|
|||
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
||||
# MovieGenre.find_or_create_by!(name: genre_name)
|
||||
# end
|
||||
|
||||
if Rails.env.development?
|
||||
user = User.first || User.create!(
|
||||
hca_id: 'dev_user',
|
||||
email: 'dev@example.com',
|
||||
name: 'Dev User'
|
||||
)
|
||||
|
||||
provenances = [:web, :api, :slack, :rescued]
|
||||
|
||||
10.times do |i|
|
||||
# Create dummy file content
|
||||
content = "This is test file #{i} content. " * 100
|
||||
|
||||
# Create blob (ActiveStorage handles file storage to ./storage/ in dev)
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(content),
|
||||
filename: "test_file_#{i}.jpg",
|
||||
content_type: 'image/jpeg'
|
||||
)
|
||||
|
||||
# Create upload record
|
||||
provenance = provenances.sample
|
||||
Upload.create!(
|
||||
user: user,
|
||||
blob: blob,
|
||||
provenance: provenance,
|
||||
original_url: provenance == :rescued ? "https://hel1.cdn.hackclub.com/old_file_#{i}.jpg" : nil,
|
||||
created_at: rand(30.days.ago..Time.current)
|
||||
)
|
||||
end
|
||||
|
||||
puts "Created #{Upload.count} sample uploads for #{user.name}"
|
||||
puts "Provenance breakdown:"
|
||||
Upload.group(:provenance).count.each do |prov, count|
|
||||
puts " #{prov}: #{count}"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
"vite-plugin-ruby": "5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hotwired/turbo": "^8.0.22",
|
||||
"@primer/css": "^22.1.0",
|
||||
"@primer/primitives": "^11.3.2",
|
||||
"@primer/view-components": "^0.49.0"
|
||||
"@primer/view-components": "^0.49.0",
|
||||
"@rails/ujs": "^7.1.3-4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ export default defineConfig({
|
|||
minify: false
|
||||
},
|
||||
|
||||
resolve: {
|
||||
dedupe: ['@primer/view-components', '@github/catalyst']
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: ['@primer/view-components']
|
||||
},
|
||||
|
||||
server: {
|
||||
hmr: {
|
||||
overlay: true
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -201,6 +201,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@github/tab-container-element/-/tab-container-element-3.4.0.tgz#0a167ed81c9f4a1c1c79f0d0a6a8c012c49a6147"
|
||||
integrity sha512-Yx70pO8A0p7Stnm9knKkUNX8i4bjuwDYZarRkM8JH0Z+ffhpe++oNAPbzGI9GEcGugRHvKuSC6p4YOdoHtTniQ==
|
||||
|
||||
"@hotwired/turbo@^8.0.22":
|
||||
version "8.0.22"
|
||||
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.22.tgz#f6f8c0756f48ce83c31675998d55c37fdf1a6828"
|
||||
integrity sha512-A7M8vBgsmZ8W55IOEhTN7o0++zaJTavyGa1DW1rt+/b4VTr8QUWC/zu8wxMmwqvczaOe1nI/MkMq4lm2NmiHPg==
|
||||
|
||||
"@lit-labs/ssr-dom-shim@^1.2.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz#3166900c0d481f03d6d4133686e0febf760d521d"
|
||||
|
|
@ -362,6 +367,11 @@
|
|||
"@primer/behaviors" "^1.3.4"
|
||||
"@primer/live-region-element" "^0.8.0"
|
||||
|
||||
"@rails/ujs@^7.1.3-4":
|
||||
version "7.1.3-4"
|
||||
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.3-4.tgz#1dddea99d5c042e8513973ea709b2cb7e840dc2d"
|
||||
integrity sha512-z0ckI5jrAJfImcObjMT1RBz2IxH6I5q6ZTMFex6AfxSQKZuuL8JxAXvg2CvBuodGCxKvybFVolDyMHXlBLeYAA==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.57.0":
|
||||
version "4.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz#f762035679a6b168138c94c960fda0b0cdb00d98"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue