firt tests forpost file thumbnail spec
This commit is contained in:
@@ -13,7 +13,8 @@ class Domain::PostFile < ReduxApplicationRecord
|
|||||||
has_many :thumbnails,
|
has_many :thumbnails,
|
||||||
class_name: "::Domain::PostFileThumbnail",
|
class_name: "::Domain::PostFileThumbnail",
|
||||||
foreign_key: :post_file_id,
|
foreign_key: :post_file_id,
|
||||||
dependent: :destroy
|
dependent: :destroy,
|
||||||
|
inverse_of: :post_file
|
||||||
|
|
||||||
attr_json :state, :string
|
attr_json :state, :string
|
||||||
attr_json :url_str, :string
|
attr_json :url_str, :string
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ class Domain::PostFileThumbnail < ReduxApplicationRecord
|
|||||||
|
|
||||||
belongs_to :post_file,
|
belongs_to :post_file,
|
||||||
foreign_key: :post_file_id,
|
foreign_key: :post_file_id,
|
||||||
class_name: "::Domain::PostFile"
|
class_name: "::Domain::PostFile",
|
||||||
|
inverse_of: :thumbnails
|
||||||
|
|
||||||
has_many :perceptual_hashes,
|
has_many :perceptual_hashes,
|
||||||
class_name: "::Domain::PerceptualHash",
|
class_name: "::Domain::PerceptualHash",
|
||||||
@@ -18,30 +19,84 @@ class Domain::PostFileThumbnail < ReduxApplicationRecord
|
|||||||
scope: :post_file_id,
|
scope: :post_file_id,
|
||||||
},
|
},
|
||||||
inclusion: {
|
inclusion: {
|
||||||
in: THUMBNAIL_TYPES.keys,
|
in: Domain::ThumbnailType.values.map(&:name),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Thumbnail types for different uses
|
TMP_DIR = T.let(File.join(BlobFile::ROOT_DIR, "tmp-files"), String)
|
||||||
THUMBNAIL_TYPES =
|
|
||||||
|
THUMBNAIL_ROOT_DIR =
|
||||||
|
T.let(File.join(BlobFile::ROOT_DIR, "post_file_thumbnails"), String)
|
||||||
|
|
||||||
|
THUMBNAIL_CONTENT_TYPES =
|
||||||
T.let(
|
T.let(
|
||||||
{
|
[
|
||||||
small: {
|
%r{image/jpeg},
|
||||||
width: 128,
|
%r{image/jpg},
|
||||||
height: 128,
|
%r{image/png},
|
||||||
},
|
%r{image/gif},
|
||||||
medium: {
|
%r{image/webp},
|
||||||
width: 256,
|
],
|
||||||
height: 256,
|
T::Array[Regexp],
|
||||||
},
|
|
||||||
large: {
|
|
||||||
width: 512,
|
|
||||||
height: 512,
|
|
||||||
},
|
|
||||||
phash: {
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
}, # Special size for perceptual hashing
|
|
||||||
},
|
|
||||||
T::Hash[Symbol, T::Hash[Symbol, Integer]],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sig { returns(T.nilable(String)) }
|
||||||
|
def absolute_file_path
|
||||||
|
return nil unless thumbnail_type = self.thumbnail_type
|
||||||
|
return nil unless post_file_id = self.post_file&.id
|
||||||
|
return nil unless sha256 = self.post_file&.blob_sha256
|
||||||
|
sha256_hex = HexUtil.bin2hex(sha256)
|
||||||
|
path_segments = [
|
||||||
|
THUMBNAIL_ROOT_DIR,
|
||||||
|
thumbnail_type,
|
||||||
|
*BlobFile.path_segments([2, 2, 1], sha256_hex),
|
||||||
|
]
|
||||||
|
path_segments[-1] = "#{path_segments[-1]}.jpeg"
|
||||||
|
path_segments.join("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
sig do
|
||||||
|
params(
|
||||||
|
post_file: Domain::PostFile,
|
||||||
|
thumbnail_type: Domain::ThumbnailType,
|
||||||
|
).returns(T.nilable(Domain::PostFileThumbnail))
|
||||||
|
end
|
||||||
|
def self.find_or_create_from_post_file(post_file, thumbnail_type)
|
||||||
|
if t = find_by(post_file: post_file, thumbnail_type: thumbnail_type.name)
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
return nil unless post_file.state_ok?
|
||||||
|
return nil unless log_entry = post_file.log_entry
|
||||||
|
unless THUMBNAIL_CONTENT_TYPES.any? { |regex|
|
||||||
|
regex.match?(log_entry.content_type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path = post_file.blob&.absolute_file_path
|
||||||
|
return nil unless file_path
|
||||||
|
|
||||||
|
thumbnail =
|
||||||
|
Domain::PostFileThumbnail.new(
|
||||||
|
post_file: post_file,
|
||||||
|
thumbnail_type: thumbnail_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
thumbnail_path = thumbnail.absolute_file_path.to_s
|
||||||
|
unless File.exist?(thumbnail_path)
|
||||||
|
FileUtils.mkdir_p(File.dirname(thumbnail_path))
|
||||||
|
tmp_file_path = File.join(TMP_DIR, "thumbnail-#{SecureRandom.uuid}.jpeg")
|
||||||
|
image_data =
|
||||||
|
Vips::Image.thumbnail(
|
||||||
|
file_path,
|
||||||
|
thumbnail_type.width,
|
||||||
|
height: thumbnail_type.height,
|
||||||
|
size: :force,
|
||||||
|
)
|
||||||
|
image_data.jpegsave(tmp_file_path, Q: thumbnail_type.quality, strip: true)
|
||||||
|
FileUtils.mv(tmp_file_path, thumbnail_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
thumbnail.save!
|
||||||
|
thumbnail
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
50
app/models/domain/thumbnail_type.rb
Normal file
50
app/models/domain/thumbnail_type.rb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# typed: strict
|
||||||
|
# Thumbnail types for different uses
|
||||||
|
class Domain::ThumbnailType < T::Enum
|
||||||
|
extend T::Sig
|
||||||
|
|
||||||
|
enums do
|
||||||
|
Small = new
|
||||||
|
Medium = new
|
||||||
|
Large = new
|
||||||
|
PHash = new
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { returns(String) }
|
||||||
|
def name
|
||||||
|
case self
|
||||||
|
when Small
|
||||||
|
"small"
|
||||||
|
when Medium
|
||||||
|
"medium"
|
||||||
|
when Large
|
||||||
|
"large"
|
||||||
|
when PHash
|
||||||
|
"phash"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { returns(Integer) }
|
||||||
|
def width
|
||||||
|
case self
|
||||||
|
when Small
|
||||||
|
128
|
||||||
|
when Medium
|
||||||
|
256
|
||||||
|
when Large
|
||||||
|
512
|
||||||
|
when PHash
|
||||||
|
64
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { returns(Integer) }
|
||||||
|
def height
|
||||||
|
width
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { returns(Integer) }
|
||||||
|
def quality
|
||||||
|
70
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -24,6 +24,16 @@ class IpAddressRole < ReduxApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def admin?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def moderator?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Custom validation to prevent overlapping IP ranges
|
# Custom validation to prevent overlapping IP ranges
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class State::IpAddressRolePolicy < ApplicationPolicy
|
|||||||
|
|
||||||
sig { returns(T.untyped) }
|
sig { returns(T.untyped) }
|
||||||
def resolve
|
def resolve
|
||||||
if @user&.admin? || @controller.current_ip_address_role&.admin?
|
if @user&.admin?
|
||||||
@scope
|
@scope
|
||||||
else
|
else
|
||||||
@scope.where(id: nil) # Returns empty relation
|
@scope.where(id: nil) # Returns empty relation
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
test:
|
test: tmp/blob_files_test
|
||||||
tmp/blob_files_test
|
|
||||||
|
|
||||||
development:
|
development: /mnt/blob_files_development
|
||||||
/mnt/blob_files_development
|
|
||||||
|
|
||||||
staging:
|
staging: /mnt/blob_files_production
|
||||||
/mnt/blob_files_production
|
|
||||||
|
|
||||||
production:
|
production: /mnt/blob_files_production
|
||||||
/mnt/blob_files_production
|
|
||||||
|
|
||||||
worker:
|
worker: /mnt/blob_files_production
|
||||||
/mnt/blob_files_production
|
|
||||||
|
|||||||
141
db/structure.sql
141
db/structure.sql
@@ -2696,6 +2696,71 @@ CREATE SEQUENCE public.domain_inkbunny_users_id_seq
|
|||||||
ALTER SEQUENCE public.domain_inkbunny_users_id_seq OWNED BY public.domain_inkbunny_users.id;
|
ALTER SEQUENCE public.domain_inkbunny_users_id_seq OWNED BY public.domain_inkbunny_users.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_perceptual_hashes; Type: TABLE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.domain_perceptual_hashes (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
thumbnail_id bigint NOT NULL,
|
||||||
|
algorithm character varying NOT NULL,
|
||||||
|
hash_value public.vector,
|
||||||
|
created_at timestamp(6) without time zone NOT NULL,
|
||||||
|
updated_at timestamp(6) without time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_perceptual_hashes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.domain_perceptual_hashes_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_perceptual_hashes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.domain_perceptual_hashes_id_seq OWNED BY public.domain_perceptual_hashes.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_post_file_thumbnails; Type: TABLE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.domain_post_file_thumbnails (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
post_file_id bigint NOT NULL,
|
||||||
|
thumbnail_type character varying NOT NULL,
|
||||||
|
created_at timestamp(6) without time zone NOT NULL,
|
||||||
|
updated_at timestamp(6) without time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_post_file_thumbnails_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.domain_post_file_thumbnails_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_post_file_thumbnails_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.domain_post_file_thumbnails_id_seq OWNED BY public.domain_post_file_thumbnails.id;
|
||||||
|
|
||||||
|
|
||||||
SET default_tablespace = mirai;
|
SET default_tablespace = mirai;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -4588,6 +4653,20 @@ ALTER TABLE ONLY public.domain_inkbunny_tags ALTER COLUMN id SET DEFAULT nextval
|
|||||||
ALTER TABLE ONLY public.domain_inkbunny_users ALTER COLUMN id SET DEFAULT nextval('public.domain_inkbunny_users_id_seq'::regclass);
|
ALTER TABLE ONLY public.domain_inkbunny_users ALTER COLUMN id SET DEFAULT nextval('public.domain_inkbunny_users_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_perceptual_hashes id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.domain_perceptual_hashes ALTER COLUMN id SET DEFAULT nextval('public.domain_perceptual_hashes_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_post_file_thumbnails id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.domain_post_file_thumbnails ALTER COLUMN id SET DEFAULT nextval('public.domain_post_file_thumbnails_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: domain_post_files id; Type: DEFAULT; Schema: public; Owner: -
|
-- Name: domain_post_files id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -5370,6 +5449,22 @@ ALTER TABLE ONLY public.domain_inkbunny_users
|
|||||||
ADD CONSTRAINT domain_inkbunny_users_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT domain_inkbunny_users_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_perceptual_hashes domain_perceptual_hashes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.domain_perceptual_hashes
|
||||||
|
ADD CONSTRAINT domain_perceptual_hashes_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_post_file_thumbnails domain_post_file_thumbnails_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.domain_post_file_thumbnails
|
||||||
|
ADD CONSTRAINT domain_post_file_thumbnails_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
SET default_tablespace = mirai;
|
SET default_tablespace = mirai;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -6971,6 +7066,34 @@ CREATE UNIQUE INDEX index_domain_inkbunny_users_on_ib_user_id ON public.domain_i
|
|||||||
CREATE INDEX index_domain_inkbunny_users_on_shallow_update_log_entry_id ON public.domain_inkbunny_users USING btree (shallow_update_log_entry_id);
|
CREATE INDEX index_domain_inkbunny_users_on_shallow_update_log_entry_id ON public.domain_inkbunny_users USING btree (shallow_update_log_entry_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_domain_perceptual_hashes_on_algorithm; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_domain_perceptual_hashes_on_algorithm ON public.domain_perceptual_hashes USING btree (algorithm);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_domain_perceptual_hashes_on_thumbnail_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_domain_perceptual_hashes_on_thumbnail_id ON public.domain_perceptual_hashes USING btree (thumbnail_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_domain_post_file_thumbnails_on_post_file_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_domain_post_file_thumbnails_on_post_file_id ON public.domain_post_file_thumbnails USING btree (post_file_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_domain_post_file_thumbnails_on_thumbnail_type; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_domain_post_file_thumbnails_on_thumbnail_type ON public.domain_post_file_thumbnails USING btree (thumbnail_type);
|
||||||
|
|
||||||
|
|
||||||
SET default_tablespace = mirai;
|
SET default_tablespace = mirai;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -8434,6 +8557,14 @@ ALTER TABLE ONLY public.domain_fa_follows
|
|||||||
ADD CONSTRAINT fk_rails_175679b7a2 FOREIGN KEY (followed_id) REFERENCES public.domain_fa_users(id);
|
ADD CONSTRAINT fk_rails_175679b7a2 FOREIGN KEY (followed_id) REFERENCES public.domain_fa_users(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_perceptual_hashes fk_rails_1ae1a89060; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.domain_perceptual_hashes
|
||||||
|
ADD CONSTRAINT fk_rails_1ae1a89060 FOREIGN KEY (thumbnail_id) REFERENCES public.domain_post_file_thumbnails(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: domain_post_group_joins fk_rails_22154fb920; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: domain_post_group_joins fk_rails_22154fb920; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -8722,6 +8853,14 @@ ALTER TABLE ONLY public.domain_e621_taggings
|
|||||||
ADD CONSTRAINT fk_rails_da3a488297 FOREIGN KEY (post_id) REFERENCES public.domain_e621_posts(id);
|
ADD CONSTRAINT fk_rails_da3a488297 FOREIGN KEY (post_id) REFERENCES public.domain_e621_posts(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: domain_post_file_thumbnails fk_rails_dde88b4af5; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.domain_post_file_thumbnails
|
||||||
|
ADD CONSTRAINT fk_rails_dde88b4af5 FOREIGN KEY (post_file_id) REFERENCES public.domain_post_files(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: domain_inkbunny_follows fk_rails_dffb743e89; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: domain_inkbunny_follows fk_rails_dffb743e89; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -8786,6 +8925,8 @@ SET search_path TO "$user", public;
|
|||||||
|
|
||||||
INSERT INTO "schema_migrations" (version) VALUES
|
INSERT INTO "schema_migrations" (version) VALUES
|
||||||
('20250302074924'),
|
('20250302074924'),
|
||||||
|
('20250301000002'),
|
||||||
|
('20250301000001'),
|
||||||
('20250226003653'),
|
('20250226003653'),
|
||||||
('20250222035939'),
|
('20250222035939'),
|
||||||
('20250206224121'),
|
('20250206224121'),
|
||||||
|
|||||||
2
sorbet/rbi/dsl/application_controller.rbi
generated
2
sorbet/rbi/dsl/application_controller.rbi
generated
@@ -65,7 +65,7 @@ class ApplicationController
|
|||||||
sig { params(scope: T.untyped).returns(T.untyped) }
|
sig { params(scope: T.untyped).returns(T.untyped) }
|
||||||
def pundit_policy_scope(scope); end
|
def pundit_policy_scope(scope); end
|
||||||
|
|
||||||
sig { returns(T.untyped) }
|
sig { returns(T.nilable(T.any(::IpAddressRole, ::User))) }
|
||||||
def pundit_user; end
|
def pundit_user; end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
1407
sorbet/rbi/dsl/domain/perceptual_hash.rbi
generated
Normal file
1407
sorbet/rbi/dsl/domain/perceptual_hash.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
sorbet/rbi/dsl/domain/post_file.rbi
generated
14
sorbet/rbi/dsl/domain/post_file.rbi
generated
@@ -533,6 +533,20 @@ class Domain::PostFile
|
|||||||
|
|
||||||
sig { void }
|
sig { void }
|
||||||
def reset_post; end
|
def reset_post; end
|
||||||
|
|
||||||
|
sig { returns(T::Array[T.untyped]) }
|
||||||
|
def thumbnail_ids; end
|
||||||
|
|
||||||
|
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
||||||
|
def thumbnail_ids=(ids); end
|
||||||
|
|
||||||
|
# This method is created by ActiveRecord on the `Domain::PostFile` class because it declared `has_many :thumbnails`.
|
||||||
|
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
|
||||||
|
sig { returns(::Domain::PostFileThumbnail::PrivateCollectionProxy) }
|
||||||
|
def thumbnails; end
|
||||||
|
|
||||||
|
sig { params(value: T::Enumerable[::Domain::PostFileThumbnail]).void }
|
||||||
|
def thumbnails=(value); end
|
||||||
end
|
end
|
||||||
|
|
||||||
module GeneratedAssociationRelationMethods
|
module GeneratedAssociationRelationMethods
|
||||||
|
|||||||
@@ -516,6 +516,20 @@ class Domain::PostFile::InkbunnyPostFile
|
|||||||
|
|
||||||
sig { void }
|
sig { void }
|
||||||
def reset_post; end
|
def reset_post; end
|
||||||
|
|
||||||
|
sig { returns(T::Array[T.untyped]) }
|
||||||
|
def thumbnail_ids; end
|
||||||
|
|
||||||
|
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
||||||
|
def thumbnail_ids=(ids); end
|
||||||
|
|
||||||
|
# This method is created by ActiveRecord on the `Domain::PostFile` class because it declared `has_many :thumbnails`.
|
||||||
|
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
|
||||||
|
sig { returns(::Domain::PostFileThumbnail::PrivateCollectionProxy) }
|
||||||
|
def thumbnails; end
|
||||||
|
|
||||||
|
sig { params(value: T::Enumerable[::Domain::PostFileThumbnail]).void }
|
||||||
|
def thumbnails=(value); end
|
||||||
end
|
end
|
||||||
|
|
||||||
module GeneratedAssociationRelationMethods
|
module GeneratedAssociationRelationMethods
|
||||||
|
|||||||
1369
sorbet/rbi/dsl/domain/post_file_thumbnail.rbi
generated
Normal file
1369
sorbet/rbi/dsl/domain/post_file_thumbnail.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
sorbet/rbi/dsl/generated_path_helpers_module.rbi
generated
3
sorbet/rbi/dsl/generated_path_helpers_module.rbi
generated
@@ -213,6 +213,9 @@ module GeneratedPathHelpersModule
|
|||||||
sig { params(args: T.untyped).returns(String) }
|
sig { params(args: T.untyped).returns(String) }
|
||||||
def stats_log_entries_path(*args); end
|
def stats_log_entries_path(*args); end
|
||||||
|
|
||||||
|
sig { params(args: T.untyped).returns(String) }
|
||||||
|
def toggle_state_ip_address_role_path(*args); end
|
||||||
|
|
||||||
sig { params(args: T.untyped).returns(String) }
|
sig { params(args: T.untyped).returns(String) }
|
||||||
def turbo_recede_historical_location_path(*args); end
|
def turbo_recede_historical_location_path(*args); end
|
||||||
|
|
||||||
|
|||||||
3
sorbet/rbi/dsl/generated_url_helpers_module.rbi
generated
3
sorbet/rbi/dsl/generated_url_helpers_module.rbi
generated
@@ -213,6 +213,9 @@ module GeneratedUrlHelpersModule
|
|||||||
sig { params(args: T.untyped).returns(String) }
|
sig { params(args: T.untyped).returns(String) }
|
||||||
def stats_log_entries_url(*args); end
|
def stats_log_entries_url(*args); end
|
||||||
|
|
||||||
|
sig { params(args: T.untyped).returns(String) }
|
||||||
|
def toggle_state_ip_address_role_url(*args); end
|
||||||
|
|
||||||
sig { params(args: T.untyped).returns(String) }
|
sig { params(args: T.untyped).returns(String) }
|
||||||
def turbo_recede_historical_location_url(*args); end
|
def turbo_recede_historical_location_url(*args); end
|
||||||
|
|
||||||
|
|||||||
36
sorbet/rbi/dsl/ip_address_role.rbi
generated
36
sorbet/rbi/dsl/ip_address_role.rbi
generated
@@ -393,18 +393,6 @@ class IpAddressRole
|
|||||||
end
|
end
|
||||||
|
|
||||||
module EnumMethodsModule
|
module EnumMethodsModule
|
||||||
sig { void }
|
|
||||||
def admin!; end
|
|
||||||
|
|
||||||
sig { returns(T::Boolean) }
|
|
||||||
def admin?; end
|
|
||||||
|
|
||||||
sig { void }
|
|
||||||
def moderator!; end
|
|
||||||
|
|
||||||
sig { returns(T::Boolean) }
|
|
||||||
def moderator?; end
|
|
||||||
|
|
||||||
sig { void }
|
sig { void }
|
||||||
def user!; end
|
def user!; end
|
||||||
|
|
||||||
@@ -413,9 +401,6 @@ class IpAddressRole
|
|||||||
end
|
end
|
||||||
|
|
||||||
module GeneratedAssociationRelationMethods
|
module GeneratedAssociationRelationMethods
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
|
||||||
def admin(*args, &blk); end
|
|
||||||
|
|
||||||
sig { returns(PrivateAssociationRelation) }
|
sig { returns(PrivateAssociationRelation) }
|
||||||
def all; end
|
def all; end
|
||||||
|
|
||||||
@@ -485,18 +470,9 @@ class IpAddressRole
|
|||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
||||||
def merge(*args, &blk); end
|
def merge(*args, &blk); end
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
|
||||||
def moderator(*args, &blk); end
|
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
||||||
def none(*args, &blk); end
|
def none(*args, &blk); end
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
|
||||||
def not_admin(*args, &blk); end
|
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
|
||||||
def not_moderator(*args, &blk); end
|
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
|
||||||
def not_user(*args, &blk); end
|
def not_user(*args, &blk); end
|
||||||
|
|
||||||
@@ -1082,9 +1058,6 @@ class IpAddressRole
|
|||||||
end
|
end
|
||||||
|
|
||||||
module GeneratedRelationMethods
|
module GeneratedRelationMethods
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
|
||||||
def admin(*args, &blk); end
|
|
||||||
|
|
||||||
sig { returns(PrivateRelation) }
|
sig { returns(PrivateRelation) }
|
||||||
def all; end
|
def all; end
|
||||||
|
|
||||||
@@ -1154,18 +1127,9 @@ class IpAddressRole
|
|||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
||||||
def merge(*args, &blk); end
|
def merge(*args, &blk); end
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
|
||||||
def moderator(*args, &blk); end
|
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
||||||
def none(*args, &blk); end
|
def none(*args, &blk); end
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
|
||||||
def not_admin(*args, &blk); end
|
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
|
||||||
def not_moderator(*args, &blk); end
|
|
||||||
|
|
||||||
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
|
||||||
def not_user(*args, &blk); end
|
def not_user(*args, &blk); end
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,27 @@ class Vips::Image
|
|||||||
sig { params(name: String, n: T.nilable(Integer)).returns(Vips::Image) }
|
sig { params(name: String, n: T.nilable(Integer)).returns(Vips::Image) }
|
||||||
def self.gifload(name, n: nil)
|
def self.gifload(name, n: nil)
|
||||||
end
|
end
|
||||||
|
sig do
|
||||||
|
params(
|
||||||
|
filename: String,
|
||||||
|
width: Integer,
|
||||||
|
height: T.nilable(Integer),
|
||||||
|
# :up, :down, :both, :force
|
||||||
|
size: T.nilable(Symbol),
|
||||||
|
).returns(Vips::Image)
|
||||||
|
end
|
||||||
|
def self.thumbnail(filename, width, height: nil, size: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
sig do
|
||||||
|
params(
|
||||||
|
width: Integer,
|
||||||
|
height: T.nilable(Integer),
|
||||||
|
size: T.nilable(Symbol),
|
||||||
|
).returns(Vips::Image)
|
||||||
|
end
|
||||||
|
def thumbnail_image(width, height: nil, size: nil)
|
||||||
|
end
|
||||||
|
|
||||||
sig { params(scale: T.untyped).returns(Vips::Image) }
|
sig { params(scale: T.untyped).returns(Vips::Image) }
|
||||||
def resize(scale)
|
def resize(scale)
|
||||||
@@ -16,9 +37,7 @@ class Vips::Image
|
|||||||
def gifsave_buffer(**opts)
|
def gifsave_buffer(**opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
sig do
|
sig { params(filename: String, opts: T.untyped).returns(String) }
|
||||||
params(width: Integer, height: T.nilable(Integer)).returns(Vips::Image)
|
def jpegsave(filename, **opts)
|
||||||
end
|
|
||||||
def thumbnail_image(width, height: nil)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
7
spec/factories/domain/post_file_thumbnail.rb
Normal file
7
spec/factories/domain/post_file_thumbnail.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# typed: false
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :domain_post_file_thumbnail, class: "Domain::PostFileThumbnail" do
|
||||||
|
association :post_file, factory: :domain_post_file
|
||||||
|
thumbnail_type { Domain::ThumbnailType::Small.name }
|
||||||
|
end
|
||||||
|
end
|
||||||
171
spec/models/domain/post_file_thumbnail_spec.rb
Normal file
171
spec/models/domain/post_file_thumbnail_spec.rb
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# typed: false
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Domain::PostFileThumbnail do
|
||||||
|
describe ".find_or_create_from_post_file" do
|
||||||
|
let(:post) { create(:domain_post_fa_post) }
|
||||||
|
let(:blob_file) do
|
||||||
|
fixture_path =
|
||||||
|
Rails.root.join(
|
||||||
|
"test/fixtures/files/images/thumb-036aaab6-content-container.jpeg",
|
||||||
|
)
|
||||||
|
create(
|
||||||
|
:blob_file,
|
||||||
|
contents: File.binread(fixture_path),
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let(:log_entry) do
|
||||||
|
create(:http_log_entry, content_type: "image/jpeg", response: blob_file)
|
||||||
|
end
|
||||||
|
let(:post_file) do
|
||||||
|
create(:domain_post_file, post: post, state: "ok", log_entry: log_entry)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Ensure directories exist
|
||||||
|
FileUtils.mkdir_p(Domain::PostFileThumbnail::TMP_DIR)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a thumbnail for a valid post file" do
|
||||||
|
thumbnail =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(thumbnail).to be_a(Domain::PostFileThumbnail)
|
||||||
|
expect(thumbnail.post_file).to eq(post_file)
|
||||||
|
expect(thumbnail.thumbnail_type).to eq("small")
|
||||||
|
|
||||||
|
# Verify the thumbnail file was created on disk
|
||||||
|
thumbnail_path = thumbnail.absolute_file_path
|
||||||
|
expect(thumbnail_path).not_to be_nil
|
||||||
|
expect(File.exist?(thumbnail_path)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns existing thumbnail if one already exists" do
|
||||||
|
# Create a thumbnail first
|
||||||
|
existing =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
expect(existing).not_to be_nil
|
||||||
|
|
||||||
|
# Try to find or create again
|
||||||
|
thumbnail =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return the existing one without creating a new one
|
||||||
|
expect(thumbnail).to eq(existing)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for a post file that's not in 'ok' state" do
|
||||||
|
post_file.update!(state: "pending")
|
||||||
|
|
||||||
|
thumbnail =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(thumbnail).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for a post file without a log entry" do
|
||||||
|
post_file.update!(log_entry: nil)
|
||||||
|
|
||||||
|
thumbnail =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(thumbnail).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for a post file with an unsupported content type" do
|
||||||
|
# Create a new log entry with PDF content type since HttpLogEntry is immutable
|
||||||
|
pdf_blob =
|
||||||
|
create(
|
||||||
|
:blob_file,
|
||||||
|
content_type: "application/pdf",
|
||||||
|
contents: "fake pdf data",
|
||||||
|
)
|
||||||
|
pdf_log_entry =
|
||||||
|
create(
|
||||||
|
:http_log_entry,
|
||||||
|
content_type: "application/pdf",
|
||||||
|
response: pdf_blob,
|
||||||
|
)
|
||||||
|
pdf_post_file =
|
||||||
|
create(
|
||||||
|
:domain_post_file,
|
||||||
|
post: post,
|
||||||
|
state: "ok",
|
||||||
|
log_entry: pdf_log_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
thumbnail =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
pdf_post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(thumbnail).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil if the post file has no blob" do
|
||||||
|
allow(post_file).to receive(:blob).and_return(nil)
|
||||||
|
|
||||||
|
thumbnail =
|
||||||
|
described_class.find_or_create_from_post_file(
|
||||||
|
post_file,
|
||||||
|
Domain::ThumbnailType::Small,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(thumbnail).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#absolute_file_path" do
|
||||||
|
let(:post) { create(:domain_post_fa_post) }
|
||||||
|
let(:blob_file) do
|
||||||
|
create(
|
||||||
|
:blob_file,
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
contents: "fake image data",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let(:post_file) do
|
||||||
|
create(:domain_post_file, post: post, state: "ok", blob: blob_file)
|
||||||
|
end
|
||||||
|
let(:thumbnail) do
|
||||||
|
create(
|
||||||
|
:domain_post_file_thumbnail,
|
||||||
|
post_file: post_file,
|
||||||
|
thumbnail_type: "small",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "constructs the correct file path" do
|
||||||
|
expect(thumbnail.absolute_file_path).to start_with(
|
||||||
|
Domain::PostFileThumbnail::THUMBNAIL_ROOT_DIR,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil if thumbnail_type is nil" do
|
||||||
|
thumbnail.thumbnail_type = nil
|
||||||
|
expect(thumbnail.absolute_file_path).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil if post_file is nil" do
|
||||||
|
thumbnail.post_file = nil
|
||||||
|
expect(thumbnail.absolute_file_path).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
BIN
test/fixtures/files/images/thumb-036aaab6-content-container.jpeg
vendored
Normal file
BIN
test/fixtures/files/images/thumb-036aaab6-content-container.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
Reference in New Issue
Block a user