agent-notes 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git
|
|
3
|
+
description: "Git workflow: analyzing changes, chunking commits, conventional commit messages"
|
|
4
|
+
group: git
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Git Workflow
|
|
8
|
+
|
|
9
|
+
## Committing changes
|
|
10
|
+
|
|
11
|
+
When asked to commit, follow this process:
|
|
12
|
+
|
|
13
|
+
### 1. Analyze all changes
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git status
|
|
17
|
+
git diff
|
|
18
|
+
git diff --cached
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Chunk into logical commits
|
|
22
|
+
|
|
23
|
+
Group related changes into small, focused commits. Each commit should represent ONE logical change:
|
|
24
|
+
- Don't mix refactors with features
|
|
25
|
+
- Don't mix formatting with bug fixes
|
|
26
|
+
- Separate test changes if they cover different features
|
|
27
|
+
- New file + its test = one commit is fine
|
|
28
|
+
|
|
29
|
+
### 3. Stage and commit each chunk
|
|
30
|
+
|
|
31
|
+
For each logical chunk:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git add <specific files>
|
|
35
|
+
git commit -m "#<ticket> type(scope): short description"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 4. Commit message format
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
#<ticket> type(scope): short description
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **No body, no description** — title only
|
|
45
|
+
- **Short** — under 72 characters
|
|
46
|
+
- **Lowercase** — no capital letters after the colon
|
|
47
|
+
- Extract ticket number from branch name when available
|
|
48
|
+
|
|
49
|
+
### 5. Extract ticket from branch
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git branch --show-current
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Patterns:
|
|
56
|
+
- `feature/123-description` → `#123`
|
|
57
|
+
- `fix/ABC-123-description` → `#ABC-123`
|
|
58
|
+
- `123-description` → `#123`
|
|
59
|
+
- No ticket found → omit prefix
|
|
60
|
+
|
|
61
|
+
### 6. Commit types
|
|
62
|
+
|
|
63
|
+
- `feat` — new feature or file
|
|
64
|
+
- `fix` — bug fix
|
|
65
|
+
- `refactor` — code restructuring, no behavior change
|
|
66
|
+
- `test` — test additions or changes
|
|
67
|
+
- `docs` — documentation only
|
|
68
|
+
- `chore` — config, tooling, dependencies
|
|
69
|
+
- `style` — formatting only
|
|
70
|
+
- `perf` — performance improvement
|
|
71
|
+
|
|
72
|
+
### 7. Scope
|
|
73
|
+
|
|
74
|
+
Derive from the primary area of change: `auth`, `api`, `models`, `ci`, `install`, `doctor`, etc.
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
Single commit:
|
|
79
|
+
```
|
|
80
|
+
#142 feat(auth): add two-factor authentication via TOTP
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Multiple logical chunks from one set of changes:
|
|
84
|
+
```
|
|
85
|
+
#89 fix(payments): handle nil amount in refund calculation
|
|
86
|
+
#89 test(payments): add specs for refund edge cases
|
|
87
|
+
```
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-active-storage
|
|
3
|
+
description: "Active Storage file uploads: attachments, variants, validations, and cloud storage config"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Active Storage
|
|
8
|
+
|
|
9
|
+
File upload and attachment patterns with Active Storage.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### Single Attachment
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
class User < ApplicationRecord
|
|
19
|
+
has_one_attached :avatar, dependent: :purge_later
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Multiple Attachments
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class Card < ApplicationRecord
|
|
27
|
+
has_many_attached :documents, dependent: :purge_later
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Dependent options:**
|
|
32
|
+
- `:purge` - Deletes attachment immediately when record is destroyed
|
|
33
|
+
- `:purge_later` - Queues job to delete attachment (recommended for production)
|
|
34
|
+
- `false` - Keeps attachment files orphaned (not recommended)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Uploading Files
|
|
39
|
+
|
|
40
|
+
### In Forms
|
|
41
|
+
|
|
42
|
+
```erb
|
|
43
|
+
<%= form_with model: @user do |f| %>
|
|
44
|
+
<%= f.file_field :avatar %>
|
|
45
|
+
<%= f.submit %>
|
|
46
|
+
<% end %>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Direct Uploads
|
|
50
|
+
|
|
51
|
+
```erb
|
|
52
|
+
<%= f.file_field :avatar, direct_upload: true %>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Displaying Images
|
|
58
|
+
|
|
59
|
+
```erb
|
|
60
|
+
<% if @user.avatar.attached? %>
|
|
61
|
+
<%= image_tag @user.avatar %>
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<%# With variant %>
|
|
65
|
+
<%= image_tag @user.avatar.variant(resize_to_limit: [200, 200]) %>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Variants
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Resize
|
|
74
|
+
@user.avatar.variant(resize_to_limit: [300, 300])
|
|
75
|
+
|
|
76
|
+
# Crop
|
|
77
|
+
@user.avatar.variant(resize_to_fill: [300, 300])
|
|
78
|
+
|
|
79
|
+
# Format
|
|
80
|
+
@user.avatar.variant(resize_to_limit: [300, 300], format: :jpg)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Validations
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class User < ApplicationRecord
|
|
89
|
+
has_one_attached :avatar
|
|
90
|
+
|
|
91
|
+
validate :avatar_validation
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
def avatar_validation
|
|
95
|
+
return unless avatar.attached?
|
|
96
|
+
|
|
97
|
+
unless avatar.content_type.in?(%w[image/png image/jpg image/jpeg])
|
|
98
|
+
errors.add(:avatar, "must be a PNG or JPG")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if avatar.byte_size > 5.megabytes
|
|
102
|
+
errors.add(:avatar, "must be less than 5MB")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Downloading Files
|
|
111
|
+
|
|
112
|
+
### Small Files
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# In controller
|
|
116
|
+
def download
|
|
117
|
+
send_data @user.avatar.download,
|
|
118
|
+
filename: @user.avatar.filename.to_s,
|
|
119
|
+
type: @user.avatar.content_type,
|
|
120
|
+
disposition: "attachment"
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Large Files (Streaming)
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# Redirect to cloud storage URL (recommended for large files)
|
|
128
|
+
def download
|
|
129
|
+
redirect_to rails_blob_url(@document), allow_other_host: true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Or use X-Sendfile/X-Accel-Redirect
|
|
133
|
+
# Configure your web server (nginx/apache) for efficient file serving
|
|
134
|
+
send_file @user.avatar.service_url
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Removing Attachments
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Remove single attachment
|
|
143
|
+
@user.avatar.purge # Synchronous
|
|
144
|
+
@user.avatar.purge_later # Background job (recommended)
|
|
145
|
+
|
|
146
|
+
# Remove all attachments from a collection
|
|
147
|
+
@card.documents.purge_all # Synchronous
|
|
148
|
+
@card.documents.purge_later # Background job (recommended)
|
|
149
|
+
|
|
150
|
+
# Check if attached
|
|
151
|
+
@user.avatar.attached? # => true/false
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Service Configuration
|
|
157
|
+
|
|
158
|
+
### Amazon S3
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# config/storage.yml
|
|
162
|
+
amazon:
|
|
163
|
+
service: S3
|
|
164
|
+
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
|
165
|
+
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
|
166
|
+
region: us-east-1
|
|
167
|
+
bucket: my-app-<%= Rails.env %>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Google Cloud Storage
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
# config/storage.yml
|
|
174
|
+
google:
|
|
175
|
+
service: GCS
|
|
176
|
+
project: my-project
|
|
177
|
+
credentials: <%= Rails.application.credentials.dig(:gcs, :keyfile) %>
|
|
178
|
+
bucket: my-app-<%= Rails.env %>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Microsoft Azure
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
# config/storage.yml
|
|
185
|
+
microsoft:
|
|
186
|
+
service: AzureStorage
|
|
187
|
+
storage_account_name: <%= Rails.application.credentials.dig(:azure, :storage_account_name) %>
|
|
188
|
+
storage_access_key: <%= Rails.application.credentials.dig(:azure, :storage_access_key) %>
|
|
189
|
+
container: my-app-<%= Rails.env %>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Local (Development/Test)
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
# config/storage.yml
|
|
196
|
+
local:
|
|
197
|
+
service: Disk
|
|
198
|
+
root: <%= Rails.root.join("storage") %>
|
|
199
|
+
|
|
200
|
+
# config/environments/development.rb
|
|
201
|
+
config.active_storage.service = :local
|
|
202
|
+
|
|
203
|
+
# config/environments/production.rb
|
|
204
|
+
config.active_storage.service = :amazon
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Advanced Validations
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class User < ApplicationRecord
|
|
213
|
+
has_one_attached :avatar, dependent: :purge_later
|
|
214
|
+
has_many_attached :documents, dependent: :purge_later
|
|
215
|
+
|
|
216
|
+
validate :avatar_validation
|
|
217
|
+
validate :documents_validation
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
def avatar_validation
|
|
221
|
+
return unless avatar.attached?
|
|
222
|
+
|
|
223
|
+
# Content type validation
|
|
224
|
+
acceptable_types = %w[image/png image/jpg image/jpeg image/gif]
|
|
225
|
+
unless avatar.content_type.in?(acceptable_types)
|
|
226
|
+
errors.add(:avatar, "must be a PNG, JPG, or GIF")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# File size validation
|
|
230
|
+
if avatar.byte_size > 5.megabytes
|
|
231
|
+
errors.add(:avatar, "must be less than 5MB")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Image dimensions (requires image_processing gem)
|
|
235
|
+
image = MiniMagick::Image.read(avatar.download)
|
|
236
|
+
if image.width < 100 || image.height < 100
|
|
237
|
+
errors.add(:avatar, "must be at least 100x100 pixels")
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def documents_validation
|
|
242
|
+
return unless documents.attached?
|
|
243
|
+
|
|
244
|
+
# Validate total count
|
|
245
|
+
if documents.count > 10
|
|
246
|
+
errors.add(:documents, "cannot exceed 10 files")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Validate each document
|
|
250
|
+
documents.each do |document|
|
|
251
|
+
# File size
|
|
252
|
+
if document.byte_size > 10.megabytes
|
|
253
|
+
errors.add(:documents, "#{document.filename} must be less than 10MB")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Content type
|
|
257
|
+
acceptable_types = %w[application/pdf image/png image/jpg]
|
|
258
|
+
unless document.content_type.in?(acceptable_types)
|
|
259
|
+
errors.add(:documents, "#{document.filename} must be a PDF or image")
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Virus Scanning (Production Pattern)
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# app/models/concerns/virus_scannable.rb
|
|
272
|
+
module VirusScannable
|
|
273
|
+
extend ActiveSupport::Concern
|
|
274
|
+
|
|
275
|
+
included do
|
|
276
|
+
before_save :scan_attachments
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
def scan_attachments
|
|
281
|
+
self.class.attachment_reflections.each_key do |name|
|
|
282
|
+
attachment = public_send(name)
|
|
283
|
+
next unless attachment.attached? && attachment.changed?
|
|
284
|
+
|
|
285
|
+
if attachment.is_a?(ActiveStorage::Attached::Many)
|
|
286
|
+
attachment.each { |file| scan_file(file, name) }
|
|
287
|
+
else
|
|
288
|
+
scan_file(attachment, name)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def scan_file(file, attribute_name)
|
|
294
|
+
# Example using ClamAV or similar service
|
|
295
|
+
scanner = VirusScanner.new(file)
|
|
296
|
+
if scanner.infected?
|
|
297
|
+
errors.add(attribute_name, "contains a virus")
|
|
298
|
+
file.purge
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Usage
|
|
304
|
+
class User < ApplicationRecord
|
|
305
|
+
include VirusScannable
|
|
306
|
+
has_one_attached :avatar
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Summary
|
|
313
|
+
|
|
314
|
+
- **has_one_attached**: Single file with `dependent: :purge_later`
|
|
315
|
+
- **has_many_attached**: Multiple files with proper validations
|
|
316
|
+
- **Variants**: Image transformations (resize, crop, format)
|
|
317
|
+
- **Direct Upload**: Upload to cloud storage directly (faster)
|
|
318
|
+
- **Validations**: Content type, file size, dimensions, count
|
|
319
|
+
- **Service Config**: S3, GCS, Azure for production
|
|
320
|
+
- **Streaming**: Use redirects for large file downloads
|
|
321
|
+
- **Virus Scanning**: Add security layer for uploads
|