Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby '2.7.2'
5 |
6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
7 | gem 'rails', '6.1.3'
8 | # Use postgresql as the database for Active Record
9 | gem 'pg', '>= 0.18', '< 2.0'
10 | # Use Puma as the app server
11 | gem 'puma', '~> 4.1'
12 | # Use SCSS for stylesheets
13 | gem 'sassc-rails'
14 | gem 'sprockets-rails'
15 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
16 | gem 'turbolinks', '~> 5'
17 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
18 | gem 'jbuilder', '~> 2.7'
19 | # Use Redis adapter to run Action Cable in production
20 | # gem 'redis', '~> 4.0'
21 | # Use Active Model has_secure_password
22 | # gem 'bcrypt', '~> 3.1.7'
23 |
24 | # Use Active Storage variant
25 | # gem 'image_processing', '~> 1.2'
26 |
27 | gem 'ipfs-http-client', github: 'andrew/ipfs-http-client'
28 | gem 'pagy'
29 | gem 'dotenv-rails'
30 | gem 'sidekiq'
31 | gem 'foreman'
32 | gem 'jquery-rails'
33 | gem 'bootstrap'
34 | gem 'octicons_helper'
35 | gem "chartkick"
36 | gem 'groupdate'
37 |
38 | # Reduces boot times through caching; required in config/boot.rb
39 | gem 'bootsnap', '>= 1.4.2', require: false
40 |
41 | gem 'cid', github: 'andrew/ruby-cid', branch: 'main'
42 |
43 | group :development do
44 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
45 | gem 'web-console', '>= 3.3.0'
46 | gem 'listen', '~> 3.2'
47 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
48 | gem 'spring'
49 | gem 'spring-watcher-listen', '~> 2.0.0'
50 | end
51 |
52 | group :test do
53 | gem 'rails-controller-testing'
54 | end
55 |
56 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
57 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
58 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/controllers/pins_controller.rb:
--------------------------------------------------------------------------------
1 | class PinsController < ApplicationController
2 | before_action :authenticate_user!
3 |
4 | def index
5 | @limit = [1, params[:limit].to_i, 1000].sort[1]
6 | statuses = params[:status].to_s.split(',')
7 | @status = statuses.select{|s| Pin::STATUSES.include?(s)}
8 | @status = 'pinned' if @status.blank?
9 | @scope = current_user.pins.not_deleted.order('created_at DESC').status(@status)
10 |
11 | @scope = @scope.name_contains(params[:name]) if params[:name].present?
12 | @scope = @scope.cids(params[:cid].split(',')) if params[:cid].present?
13 | @scope = @scope.before(params[:before]) if params[:before].present?
14 | @scope = @scope.after(params[:after]) if params[:after].present?
15 | @scope = @scope.meta(JSON.parse(params[:meta])) if params[:meta].present?
16 |
17 | @count = @scope.count
18 | @pagy, @pins = pagy(@scope, per: @limit)
19 | end
20 |
21 | def new
22 | @pin = current_user.pins.build
23 | end
24 |
25 | def create
26 | @pin = current_user.pins.build(pin_params)
27 | if @pin.save
28 | @pin.ipfs_add_async
29 | redirect_to @pin
30 | else
31 | render :new
32 | end
33 | end
34 |
35 | def destroy
36 | @pin = current_user.pins.not_deleted.find(params[:id])
37 | @pin.ipfs_remove_async
38 | @pin.mark_deleted
39 | redirect_to pins_path
40 | end
41 |
42 | def show
43 | @pin = current_user.pins.not_deleted.find(params[:id])
44 | end
45 |
46 | def update
47 | @existing_pin = current_user.pins.not_deleted.find(params[:id])
48 | @pin = current_user.pins.build(pin_params)
49 | if @pin.save!
50 | @pin.ipfs_add_async
51 | @existing_pin.ipfs_remove_async
52 | @existing_pin.mark_deleted
53 | redirect_to @pin
54 | else
55 | render :edit
56 | end
57 | end
58 |
59 | def edit
60 | @pin = current_user.pins.find(params[:id])
61 | end
62 |
63 | protected
64 |
65 | def pin_params
66 | params.require(:pin).permit(:cid, :name)
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # The test environment is used exclusively to run your application's
2 | # test suite. You never need to work with it otherwise. Remember that
3 | # your test database is "scratch space" for the test suite and is wiped
4 | # and recreated between test runs. Don't rely on the data there!
5 |
6 | Rails.application.configure do
7 | # Settings specified here will take precedence over those in config/application.rb.
8 |
9 | config.cache_classes = false
10 | config.action_view.cache_template_loading = true
11 |
12 | # Do not eager load code on boot. This avoids loading your whole application
13 | # just for the purpose of running a single test. If you are using a tool that
14 | # preloads Rails for running tests, you may have to set it to true.
15 | config.eager_load = false
16 |
17 | # Configure public file server for tests with Cache-Control for performance.
18 | config.public_file_server.enabled = true
19 | config.public_file_server.headers = {
20 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
21 | }
22 |
23 | # Show full error reports and disable caching.
24 | config.consider_all_requests_local = true
25 | config.action_controller.perform_caching = false
26 | config.cache_store = :null_store
27 |
28 | # Raise exceptions instead of rendering exception templates.
29 | config.action_dispatch.show_exceptions = false
30 |
31 | # Disable request forgery protection in test environment.
32 | config.action_controller.allow_forgery_protection = false
33 |
34 | # Store uploaded files on the local file system in a temporary directory.
35 | config.active_storage.service = :test
36 |
37 | config.action_mailer.perform_caching = false
38 |
39 | # Tell Action Mailer not to deliver emails to the real world.
40 | # The :test delivery method accumulates sent emails in the
41 | # ActionMailer::Base.deliveries array.
42 | config.action_mailer.delivery_method = :test
43 |
44 | # Print deprecation notices to the stderr.
45 | config.active_support.deprecation = :stderr
46 |
47 | # Raises error for missing translations.
48 | # config.action_view.raise_on_missing_translations = true
49 | end
50 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/pins_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::V1::PinsController < Api::V1::ApplicationController
2 | before_action :authenticate_user!
3 |
4 | def index
5 | @limit = [1, params[:limit].to_i, 1000].sort[1]
6 | statuses = params[:status].to_s.split(',')
7 | @status = statuses.select{|s| Pin::STATUSES.include?(s)}
8 | @status = 'pinned' if @status.blank?
9 | @scope = current_user.pins.not_deleted.order('created_at DESC').status(@status)
10 |
11 | if params[:name].present?
12 | case params[:match]
13 | when 'iexact'
14 | @scope = @scope.where('name ilike ?', params[:name])
15 | when 'partial'
16 | @scope = @scope.where('name like ?', "%#{params[:name]}%")
17 | when 'ipartial'
18 | @scope = @scope.where('name ilike ?', "%#{params[:name]}%")
19 | else
20 | @scope = @scope.where(name: params[:name])
21 | end
22 | end
23 |
24 | @scope = @scope.cids(params[:cid].split(',').first(10)) if params[:cid].present?
25 | @scope = @scope.before(params[:before]) if params[:before].present?
26 | @scope = @scope.after(params[:after]) if params[:after].present?
27 | @scope = @scope.meta(JSON.parse(params[:meta])) if params[:meta].present?
28 |
29 | @count = @scope.count
30 | @pins = @count > 0 ? @scope.limit(@limit) : []
31 | end
32 |
33 | def show
34 | @pin = current_user.pins.not_deleted.find(params[:id])
35 | end
36 |
37 | def create
38 | @pin = current_user.pins.build(pin_params)
39 | if @pin.save!
40 | @pin.ipfs_add_async
41 | end
42 | end
43 |
44 | def update
45 | @existing_pin = current_user.pins.not_deleted.find(params[:id])
46 | @pin = current_user.pins.build(pin_params)
47 | if @pin.save!
48 | @pin.ipfs_add_async
49 | @existing_pin.ipfs_remove_async
50 | @existing_pin.mark_deleted
51 | end
52 | end
53 |
54 | def destroy
55 | @pin = current_user.pins.not_deleted.find(params[:id])
56 | @pin.ipfs_remove_async
57 | @pin.mark_deleted
58 | head :accepted
59 | end
60 |
61 | protected
62 |
63 | def pin_params
64 | params.permit(:cid, :name, :origins, :meta)
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/app/models/pin.rb:
--------------------------------------------------------------------------------
1 | class Pin < ApplicationRecord
2 | STATUSES = ["queued", "pinning", "pinned", "failed", "removed"]
3 | validates_presence_of :cid
4 | validates :status, inclusion: { in: STATUSES }
5 |
6 | scope :status, ->(statuses) { where(status: statuses) }
7 | scope :cids, ->(cids) { where(cid: cids) }
8 | scope :name_contains, ->(name) { where('name ilike ?', "%#{name}%") }
9 | scope :before, ->(before) { where('created_at < ?', before) }
10 | scope :after, ->(after) { where('created_at > ?', after) }
11 | scope :meta, ->(meta) { where("meta->>? = ?", meta.first[0], meta.first[1]) }
12 | scope :not_deleted, -> { where(deleted_at: nil) }
13 |
14 | before_create :check_inlined_cids
15 |
16 | before_save :set_delegates
17 |
18 | belongs_to :user
19 |
20 | def set_delegates
21 | self.delegates = ipfs_client.id['Addresses'].reject{|a| a.match('127.0.0.1') }
22 | end
23 |
24 | def ipfs_client
25 | @client ||= Ipfs::Client.new( "http://#{ENV.fetch("IPFS_URL") { 'localhost' }}:#{ENV.fetch("IPFS_PORT") { '5001' }}")
26 | end
27 |
28 | def ipfs_add_async
29 | IpfsAddWorker.perform_async(id)
30 | end
31 |
32 | def check_inlined_cids
33 | self.status = 'pinned' if inlined_cids?
34 | end
35 |
36 | def inlined_cids?
37 | Cid.decode(cid).multihash.name == 'identity'
38 | end
39 |
40 | def ipfs_add
41 | begin
42 | update_columns(status: 'pinning') unless status == 'pinned'
43 | origins.each do |origin|
44 | ipfs_client.swarm_connect(origin)
45 | end
46 | ipfs_client.pin_add(cid)
47 | update_columns(status: 'pinned')
48 | rescue Ipfs::Commands::Error => e
49 | puts e
50 | # TODO record the exception somewhere
51 | update_columns(status: 'failed')
52 | end
53 | end
54 |
55 | def ipfs_remove_async
56 | IpfsRemoveWorker.perform_async(id)
57 | end
58 |
59 | def ipfs_remove
60 | # TODO only unpin cid if this is the only pin with that CID
61 | begin
62 | ipfs_client.pin_rm(cid)
63 | update_columns(status: 'removed')
64 | rescue Ipfs::Commands::Error => e
65 | raise unless JSON.parse(e.message)['Code'] == 0
66 | end
67 | end
68 |
69 | def info
70 | # TODO implement this
71 | {}
72 | end
73 |
74 | def mark_deleted
75 | update_columns(deleted_at: Time.zone.now)
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pins on Rails
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
9 | <%= javascript_tag 'application', 'data-turbolinks-track': 'reload' %>
10 |
11 |
12 |
13 |
14 |
42 |
43 |
44 |
45 |
46 | <% flash.each do |key, value| %>
47 |
48 | <%= value %>
49 |
50 | <% end %>
51 |
52 | <%= yield %>
53 |
54 |
55 |
56 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | # Run rails dev:cache to toggle caching.
17 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
18 | config.action_controller.perform_caching = true
19 | config.action_controller.enable_fragment_cache_logging = true
20 |
21 | config.cache_store = :memory_store
22 | config.public_file_server.headers = {
23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
24 | }
25 | else
26 | config.action_controller.perform_caching = false
27 |
28 | config.cache_store = :null_store
29 | end
30 |
31 | # Store uploaded files on the local file system (see config/storage.yml for options).
32 | config.active_storage.service = :local
33 |
34 | # Don't care if the mailer can't send.
35 | config.action_mailer.raise_delivery_errors = false
36 |
37 | config.action_mailer.perform_caching = false
38 |
39 | # Print deprecation notices to the Rails logger.
40 | config.active_support.deprecation = :log
41 |
42 | # Raise an error on page load if there are pending migrations.
43 | config.active_record.migration_error = :page_load
44 |
45 | # Highlight code that triggered database queries in logs.
46 | config.active_record.verbose_query_logs = true
47 |
48 | # Debug mode disables concatenation and preprocessing of assets.
49 | # This option may cause significant delays in view rendering with a large
50 | # number of complex assets.
51 | config.assets.debug = true
52 |
53 | # Suppress logger output for asset requests.
54 | config.assets.quiet = true
55 |
56 | # Raises error for missing translations.
57 | # config.action_view.raise_on_missing_translations = true
58 |
59 | # Use an evented file watcher to asynchronously detect changes in source code,
60 | # routes, locales, etc. This feature depends on the listen gem.
61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
62 |
63 | end
64 |
--------------------------------------------------------------------------------
/test/controllers/pins_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'test_helper'
3 |
4 | class PinsControllerTest < ActionDispatch::IntegrationTest
5 |
6 | setup do
7 | @user = User.create(email: 'test@foobar.com')
8 | end
9 |
10 | test 'creates a pin' do
11 | post '/api/v1/pins', params: {cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E'}, headers: {Authorization: "Bearer #{@user.access_token}"}
12 | assert_response :success
13 | assert_template 'pins/create'
14 | end
15 |
16 | test 'list pins for a user' do
17 | get '/api/v1/pins', headers: {Authorization: "Bearer #{@user.access_token}"}
18 | assert_response :success
19 | assert_template 'pins/index'
20 | end
21 |
22 | test 'get a pin' do
23 | @pin = @user.pins.create(cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E')
24 | get "/api/v1/pins/#{@pin.id}", headers: {Authorization: "Bearer #{@user.access_token}"}
25 | assert_response :success
26 | assert_template 'pins/show'
27 | end
28 |
29 | test 'replace a pin' do
30 | @pin = @user.pins.create(cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E')
31 | post "/api/v1/pins/#{@pin.id}", params: {cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E'}, headers: {Authorization: "Bearer #{@user.access_token}"}
32 | assert_response :success
33 | assert_template 'pins/update'
34 | end
35 |
36 | test 'destroys a pin' do
37 | @pin = @user.pins.create(cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E')
38 | delete "/api/v1/pins/#{@pin.id}", headers: {Authorization: "Bearer #{@user.access_token}"}
39 | assert_response :success
40 | end
41 |
42 | test 'pin creation requires a user' do
43 | post '/api/v1/pins', params: {cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E'}
44 | assert_response :unauthorized
45 | json = JSON.parse(response.body)
46 | assert_equal json, {"error"=>{"reason"=>"UNAUTHORIZED", "details"=>"Access token is missing or invalid"}}
47 | end
48 |
49 | test 'getting a pin requires a user' do
50 | @pin = @user.pins.create(cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E')
51 | get "/api/v1/pins/#{@pin.id}", params: {cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E'}
52 | assert_response :unauthorized
53 | json = JSON.parse(response.body)
54 | assert_equal json, {"error"=>{"reason"=>"UNAUTHORIZED", "details"=>"Access token is missing or invalid"}}
55 | end
56 |
57 | test 'getting a pin requires pin exists' do
58 | get '/api/v1/pins/999', params: {cid: 'QmUwUSmn9tCVv79WvydNfbE29YwZWy9ZNadWwtQtFKZM3E'}, headers: {Authorization: "Bearer #{@user.access_token}"}
59 | assert_response :not_found
60 | json = JSON.parse(response.body)
61 | assert_equal json, {"error" => {"reason" => "NOT_FOUND","details" =>"The specified resource was not found"}}
62 | end
63 |
64 | test 'pin creating requires cid' do
65 | post '/api/v1/pins', params: {}, headers: {Authorization: "Bearer #{@user.access_token}"}
66 | assert_response :unprocessable_entity
67 | json = JSON.parse(response.body)
68 | assert_equal json, {"error" => {"reason" => "UNPROCESSABLE_ENTITY", "details" =>"Invalid or missing parameters"}}
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "rubygems"
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($0) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV["BUNDLER_VERSION"]
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27 | bundler_version = nil
28 | update_index = nil
29 | ARGV.each_with_index do |a, i|
30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31 | bundler_version = a
32 | end
33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34 | bundler_version = $1
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV["BUNDLE_GEMFILE"]
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path("../../Gemfile", __FILE__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 | lockfile_contents = File.read(lockfile)
59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60 | Regexp.last_match(1)
61 | end
62 |
63 | def bundler_version
64 | @bundler_version ||=
65 | env_var_version || cli_arg_version ||
66 | lockfile_version
67 | end
68 |
69 | def bundler_requirement
70 | return "#{Gem::Requirement.default}.a" unless bundler_version
71 |
72 | bundler_gem_version = Gem::Version.new(bundler_version)
73 |
74 | requirement = bundler_gem_version.approximate_recommendation
75 |
76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
77 |
78 | requirement += ".a" if bundler_gem_version.prerelease?
79 |
80 | requirement
81 | end
82 |
83 | def load_bundler!
84 | ENV["BUNDLE_GEMFILE"] ||= gemfile
85 |
86 | activate_bundler
87 | end
88 |
89 | def activate_bundler
90 | gem_error = activation_error_handling do
91 | gem "bundler", bundler_requirement
92 | end
93 | return if gem_error.nil?
94 | require_error = activation_error_handling do
95 | require "bundler/version"
96 | end
97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
99 | exit 42
100 | end
101 |
102 | def activation_error_handling
103 | yield
104 | nil
105 | rescue StandardError, LoadError => e
106 | e
107 | end
108 | end
109 |
110 | m.load_bundler!
111 |
112 | if m.invoked_as_script?
113 | load Gem.bin_path("bundler", "bundle")
114 | end
115 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 9.3 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On macOS with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On macOS with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem 'pg'
16 | #
17 | default: &default
18 | username: <%= ENV.fetch("DATABASE_USERNAME") { '' } %>
19 | password: <%= ENV.fetch("DATABASE_PASSWORD") { '' } %>
20 | host: <%= ENV.fetch("DATABASE_HOST") { '' } %>
21 | port: <%= ENV.fetch("DATABASE_PORT") { '' } %>
22 | adapter: postgresql
23 | encoding: unicode
24 | # For details on connection pooling, see Rails configuration guide
25 | # https://guides.rubyonrails.org/configuring.html#database-pooling
26 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
27 |
28 | development:
29 | <<: *default
30 | database: <%= ENV.fetch("DATABASE_NAME") { 'pinning_api_development' } %>
31 |
32 | # The specified database role being used to connect to postgres.
33 | # To create additional roles in postgres see `$ createuser --help`.
34 | # When left blank, postgres will use the default role. This is
35 | # the same name as the operating system user that initialized the database.
36 | #username: pinning_api
37 |
38 | # The password associated with the postgres role (username).
39 | #password:
40 |
41 | # Connect on a TCP socket. Omitted by default since the client uses a
42 | # domain socket that doesn't need configuration. Windows does not have
43 | # domain sockets, so uncomment these lines.
44 | #host: localhost
45 |
46 | # The TCP port the server listens on. Defaults to 5432.
47 | # If your server runs on a different port number, change accordingly.
48 | #port: 5432
49 |
50 | # Schema search path. The server defaults to $user,public
51 | #schema_search_path: myapp,sharedapp,public
52 |
53 | # Minimum log levels, in increasing order:
54 | # debug5, debug4, debug3, debug2, debug1,
55 | # log, notice, warning, error, fatal, and panic
56 | # Defaults to warning.
57 | #min_messages: notice
58 |
59 | # Warning: The database defined as "test" will be erased and
60 | # re-generated from your development database when you run "rake".
61 | # Do not set this db to the same as development or production.
62 | test:
63 | <<: *default
64 | database: pinning_api_test
65 |
66 | # As with config/credentials.yml, you never want to store sensitive information,
67 | # like your database password, in your source code. If your source code is
68 | # ever seen by anyone, they now have access to your database.
69 | #
70 | # Instead, provide the password as a unix environment variable when you boot
71 | # the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
72 | # for a full rundown on how to provide these environment variables in a
73 | # production deployment.
74 | #
75 | # On Heroku and other platform providers, you may have a full connection URL
76 | # available as an environment variable. For example:
77 | #
78 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
79 | #
80 | # You can use this database configuration with:
81 | #
82 | # production:
83 | # url: <%= ENV['DATABASE_URL'] %>
84 | #
85 | production:
86 | <<: *default
87 | database: <%= ENV.fetch("DATABASE_NAME") { 'pinning_api_production' } %>
88 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
19 | # config.require_master_key = true
20 |
21 | # Disable serving static files from the `/public` folder by default since
22 | # Apache or NGINX already handles this.
23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
24 |
25 | # Compress CSS using a preprocessor.
26 | # config.assets.css_compressor = :sass
27 |
28 | # Do not fallback to assets pipeline if a precompiled asset is missed.
29 | config.assets.compile = false
30 |
31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
32 | # config.action_controller.asset_host = 'http://assets.example.com'
33 |
34 | # Specifies the header that your server uses for sending files.
35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
37 |
38 | # Store uploaded files on the local file system (see config/storage.yml for options).
39 | config.active_storage.service = :local
40 |
41 | # Mount Action Cable outside main process or domain.
42 | # config.action_cable.mount_path = nil
43 | # config.action_cable.url = 'wss://example.com/cable'
44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
45 |
46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
47 | # config.force_ssl = true
48 |
49 | # Use the lowest log level to ensure availability of diagnostic information
50 | # when problems arise.
51 | config.log_level = :debug
52 |
53 | # Prepend all log lines with the following tags.
54 | config.log_tags = [ :request_id ]
55 |
56 | # Use a different cache store in production.
57 | # config.cache_store = :mem_cache_store
58 |
59 | # Use a real queuing backend for Active Job (and separate queues per environment).
60 | # config.active_job.queue_adapter = :resque
61 | # config.active_job.queue_name_prefix = "pinning_api_production"
62 |
63 | config.action_mailer.perform_caching = false
64 |
65 | # Ignore bad email addresses and do not raise email delivery errors.
66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
67 | # config.action_mailer.raise_delivery_errors = false
68 |
69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
70 | # the I18n.default_locale when a translation cannot be found).
71 | config.i18n.fallbacks = true
72 |
73 | # Send deprecation notices to registered listeners.
74 | config.active_support.deprecation = :notify
75 |
76 | # Use default logging formatter so that PID and timestamp are not suppressed.
77 | config.log_formatter = ::Logger::Formatter.new
78 |
79 | # Use a different logger for distributed setups.
80 | # require 'syslog/logger'
81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
82 |
83 | if ENV["RAILS_LOG_TO_STDOUT"].present?
84 | logger = ActiveSupport::Logger.new(STDOUT)
85 | logger.formatter = config.log_formatter
86 | config.logger = ActiveSupport::TaggedLogging.new(logger)
87 | end
88 |
89 | # Do not dump schema after migrations.
90 | config.active_record.dump_schema_after_migration = false
91 |
92 | # Inserts middleware to perform automatic connection switching.
93 | # The `database_selector` hash is used to pass options to the DatabaseSelector
94 | # middleware. The `delay` is used to determine how long to wait after a write
95 | # to send a subsequent read to the primary.
96 | #
97 | # The `database_resolver` class is used by the middleware to determine which
98 | # database is appropriate to use based on the time delay.
99 | #
100 | # The `database_resolver_context` class is used by the middleware to set
101 | # timestamps for the last write to the primary. The resolver uses the context
102 | # class timestamps to determine how long to wait before reading from the
103 | # replica.
104 | #
105 | # By default Rails will store a last write timestamp in the session. The
106 | # DatabaseSelector middleware is designed as such you can define your own
107 | # strategy for connection switching and pass that into the middleware through
108 | # these configuration options.
109 | # config.active_record.database_selector = { delay: 2.seconds }
110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
112 | end
113 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/andrew/ipfs-http-client.git
3 | revision: 5c0e3e8b76bc624eaec6fadf9d727ed78ecbe98b
4 | specs:
5 | ipfs-http-client (0.4.0)
6 | http (~> 4.4)
7 |
8 | GIT
9 | remote: https://github.com/andrew/ruby-cid.git
10 | revision: 19bb798951f20a8e6205ee0798472cabd3e57a90
11 | branch: main
12 | specs:
13 | cid (0.1.0)
14 | base58
15 | multibases
16 | multicodecs
17 |
18 | GEM
19 | remote: https://rubygems.org/
20 | specs:
21 | actioncable (6.1.3)
22 | actionpack (= 6.1.3)
23 | activesupport (= 6.1.3)
24 | nio4r (~> 2.0)
25 | websocket-driver (>= 0.6.1)
26 | actionmailbox (6.1.3)
27 | actionpack (= 6.1.3)
28 | activejob (= 6.1.3)
29 | activerecord (= 6.1.3)
30 | activestorage (= 6.1.3)
31 | activesupport (= 6.1.3)
32 | mail (>= 2.7.1)
33 | actionmailer (6.1.3)
34 | actionpack (= 6.1.3)
35 | actionview (= 6.1.3)
36 | activejob (= 6.1.3)
37 | activesupport (= 6.1.3)
38 | mail (~> 2.5, >= 2.5.4)
39 | rails-dom-testing (~> 2.0)
40 | actionpack (6.1.3)
41 | actionview (= 6.1.3)
42 | activesupport (= 6.1.3)
43 | rack (~> 2.0, >= 2.0.9)
44 | rack-test (>= 0.6.3)
45 | rails-dom-testing (~> 2.0)
46 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
47 | actiontext (6.1.3)
48 | actionpack (= 6.1.3)
49 | activerecord (= 6.1.3)
50 | activestorage (= 6.1.3)
51 | activesupport (= 6.1.3)
52 | nokogiri (>= 1.8.5)
53 | actionview (6.1.3)
54 | activesupport (= 6.1.3)
55 | builder (~> 3.1)
56 | erubi (~> 1.4)
57 | rails-dom-testing (~> 2.0)
58 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
59 | activejob (6.1.3)
60 | activesupport (= 6.1.3)
61 | globalid (>= 0.3.6)
62 | activemodel (6.1.3)
63 | activesupport (= 6.1.3)
64 | activerecord (6.1.3)
65 | activemodel (= 6.1.3)
66 | activesupport (= 6.1.3)
67 | activestorage (6.1.3)
68 | actionpack (= 6.1.3)
69 | activejob (= 6.1.3)
70 | activerecord (= 6.1.3)
71 | activesupport (= 6.1.3)
72 | marcel (~> 0.3.1)
73 | mimemagic (~> 0.3.2)
74 | activesupport (6.1.3)
75 | concurrent-ruby (~> 1.0, >= 1.0.2)
76 | i18n (>= 1.6, < 2)
77 | minitest (>= 5.1)
78 | tzinfo (~> 2.0)
79 | zeitwerk (~> 2.3)
80 | addressable (2.7.0)
81 | public_suffix (>= 2.0.2, < 5.0)
82 | autoprefixer-rails (10.2.4.0)
83 | execjs
84 | base58 (0.2.3)
85 | bindex (0.8.1)
86 | bootsnap (1.7.2)
87 | msgpack (~> 1.0)
88 | bootstrap (4.6.0)
89 | autoprefixer-rails (>= 9.1.0)
90 | popper_js (>= 1.14.3, < 2)
91 | sassc-rails (>= 2.0.0)
92 | builder (3.2.4)
93 | chartkick (3.4.2)
94 | concurrent-ruby (1.1.8)
95 | connection_pool (2.2.3)
96 | crass (1.0.6)
97 | domain_name (0.5.20190701)
98 | unf (>= 0.0.5, < 1.0.0)
99 | dotenv (2.7.6)
100 | dotenv-rails (2.7.6)
101 | dotenv (= 2.7.6)
102 | railties (>= 3.2)
103 | erubi (1.10.0)
104 | execjs (2.7.0)
105 | ffi (1.14.2)
106 | ffi-compiler (1.0.1)
107 | ffi (>= 1.0.0)
108 | rake
109 | foreman (0.87.2)
110 | globalid (0.4.2)
111 | activesupport (>= 4.2.0)
112 | groupdate (5.2.2)
113 | activesupport (>= 5)
114 | http (4.4.1)
115 | addressable (~> 2.3)
116 | http-cookie (~> 1.0)
117 | http-form_data (~> 2.2)
118 | http-parser (~> 1.2.0)
119 | http-cookie (1.0.3)
120 | domain_name (~> 0.5)
121 | http-form_data (2.3.0)
122 | http-parser (1.2.3)
123 | ffi-compiler (>= 1.0, < 2.0)
124 | i18n (1.8.9)
125 | concurrent-ruby (~> 1.0)
126 | jbuilder (2.11.2)
127 | activesupport (>= 5.0.0)
128 | jquery-rails (4.4.0)
129 | rails-dom-testing (>= 1, < 3)
130 | railties (>= 4.2.0)
131 | thor (>= 0.14, < 2.0)
132 | listen (3.4.1)
133 | rb-fsevent (~> 0.10, >= 0.10.3)
134 | rb-inotify (~> 0.9, >= 0.9.10)
135 | loofah (2.9.0)
136 | crass (~> 1.0.2)
137 | nokogiri (>= 1.5.9)
138 | mail (2.7.1)
139 | mini_mime (>= 0.1.1)
140 | marcel (0.3.3)
141 | mimemagic (~> 0.3.2)
142 | method_source (1.0.0)
143 | mimemagic (0.3.5)
144 | mini_mime (1.0.2)
145 | mini_portile2 (2.5.0)
146 | minitest (5.14.3)
147 | msgpack (1.4.2)
148 | multibases (0.3.2)
149 | multicodecs (0.2.1)
150 | nio4r (2.5.5)
151 | nokogiri (1.11.1)
152 | mini_portile2 (~> 2.5.0)
153 | racc (~> 1.4)
154 | octicons (12.0.0)
155 | nokogiri (>= 1.6.3.1)
156 | octicons_helper (12.0.0)
157 | octicons (= 12.0.0)
158 | rails
159 | pagy (3.11.0)
160 | pg (1.2.3)
161 | popper_js (1.16.0)
162 | public_suffix (4.0.6)
163 | puma (4.3.7)
164 | nio4r (~> 2.0)
165 | racc (1.5.2)
166 | rack (2.2.3)
167 | rack-test (1.1.0)
168 | rack (>= 1.0, < 3)
169 | rails (6.1.3)
170 | actioncable (= 6.1.3)
171 | actionmailbox (= 6.1.3)
172 | actionmailer (= 6.1.3)
173 | actionpack (= 6.1.3)
174 | actiontext (= 6.1.3)
175 | actionview (= 6.1.3)
176 | activejob (= 6.1.3)
177 | activemodel (= 6.1.3)
178 | activerecord (= 6.1.3)
179 | activestorage (= 6.1.3)
180 | activesupport (= 6.1.3)
181 | bundler (>= 1.15.0)
182 | railties (= 6.1.3)
183 | sprockets-rails (>= 2.0.0)
184 | rails-controller-testing (1.0.5)
185 | actionpack (>= 5.0.1.rc1)
186 | actionview (>= 5.0.1.rc1)
187 | activesupport (>= 5.0.1.rc1)
188 | rails-dom-testing (2.0.3)
189 | activesupport (>= 4.2.0)
190 | nokogiri (>= 1.6)
191 | rails-html-sanitizer (1.3.0)
192 | loofah (~> 2.3)
193 | railties (6.1.3)
194 | actionpack (= 6.1.3)
195 | activesupport (= 6.1.3)
196 | method_source
197 | rake (>= 0.8.7)
198 | thor (~> 1.0)
199 | rake (13.0.3)
200 | rb-fsevent (0.10.4)
201 | rb-inotify (0.10.1)
202 | ffi (~> 1.0)
203 | redis (4.2.5)
204 | sassc (2.4.0)
205 | ffi (~> 1.9)
206 | sassc-rails (2.1.2)
207 | railties (>= 4.0.0)
208 | sassc (>= 2.0)
209 | sprockets (> 3.0)
210 | sprockets-rails
211 | tilt
212 | sidekiq (6.1.3)
213 | connection_pool (>= 2.2.2)
214 | rack (~> 2.0)
215 | redis (>= 4.2.0)
216 | spring (2.1.1)
217 | spring-watcher-listen (2.0.1)
218 | listen (>= 2.7, < 4.0)
219 | spring (>= 1.2, < 3.0)
220 | sprockets (4.0.2)
221 | concurrent-ruby (~> 1.0)
222 | rack (> 1, < 3)
223 | sprockets-rails (3.2.2)
224 | actionpack (>= 4.0)
225 | activesupport (>= 4.0)
226 | sprockets (>= 3.0.0)
227 | thor (1.1.0)
228 | tilt (2.0.10)
229 | turbolinks (5.2.1)
230 | turbolinks-source (~> 5.2)
231 | turbolinks-source (5.2.0)
232 | tzinfo (2.0.4)
233 | concurrent-ruby (~> 1.0)
234 | unf (0.1.4)
235 | unf_ext
236 | unf_ext (0.0.7.7)
237 | web-console (4.1.0)
238 | actionview (>= 6.0.0)
239 | activemodel (>= 6.0.0)
240 | bindex (>= 0.4.0)
241 | railties (>= 6.0.0)
242 | websocket-driver (0.7.3)
243 | websocket-extensions (>= 0.1.0)
244 | websocket-extensions (0.1.5)
245 | zeitwerk (2.4.2)
246 |
247 | PLATFORMS
248 | ruby
249 |
250 | DEPENDENCIES
251 | bootsnap (>= 1.4.2)
252 | bootstrap
253 | chartkick
254 | cid!
255 | dotenv-rails
256 | foreman
257 | groupdate
258 | ipfs-http-client!
259 | jbuilder (~> 2.7)
260 | jquery-rails
261 | listen (~> 3.2)
262 | octicons_helper
263 | pagy
264 | pg (>= 0.18, < 2.0)
265 | puma (~> 4.1)
266 | rails (= 6.1.3)
267 | rails-controller-testing
268 | sassc-rails
269 | sidekiq
270 | spring
271 | spring-watcher-listen (~> 2.0.0)
272 | sprockets-rails
273 | turbolinks (~> 5)
274 | tzinfo-data
275 | web-console (>= 3.3.0)
276 |
277 | RUBY VERSION
278 | ruby 2.7.2p137
279 |
280 | BUNDLED WITH
281 | 2.1.4
282 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IPFS Pinning Service API
2 |
3 | A rails app that implements the [IPFS Pinning Service API](https://ipfs.github.io/pinning-services-api-spec/).
4 |
5 | ## Endpoints
6 |
7 | The CRUD design of the pinning service API spec maps nicely onto the standard rails restful routing design with only a few minor tweaks needed to implement the full spec.
8 |
9 | For the API, the default format is set to `:json` rather than HTML and CSRF protection is disabled, we've also nested the API routes under /api/v1 to seperate it from any html pages and allow deploying a v2 in the future without breaking compatibility with existing v1 clients.
10 |
11 | TODO: implement authentication and access control via `Authorization: Bearer `
12 |
13 | The spec does not have any details around etag, gzip, last-modified and other headers that can help improve performance but these could be offered if the client supports them.
14 |
15 | ### [Create Pin](https://ipfs.github.io/pinning-services-api-spec/#tag/pins/paths/~1pins/post)
16 |
17 | POST request to /pins go to `pins#create`, rails will automatically parse the body as json and convert into params. Although the spec does not nest the object in a `pin` object like the ActiveRecord pattern, the keys do map nicely onto fields on our `Pin` database table.
18 |
19 | We store `origins` in a postgresql `array` field and `meta` in a postgresql `jsonb` field which gives us some validation for free, the only required field is `cid` so that validation is set on the `Pin` model and `NOT NULL` at the database level. The `status` is defaulted to `queued` on newly created `Pin` records.
20 |
21 | Before saving the `Pin` to the database, we also set the address of our local IPFS node in the `delegates` field, fetched using `ipfs id`.
22 |
23 | After successful validation, the following steps are made to communicate with local IPFS node via http to pin the CID (pseudo ipfs commands included inline):
24 |
25 | - set `status` to `pinning`
26 | - attempt to connect to each `origin` (`ipfs swarm connect #{origin address}`)
27 | - pin the `cid` (`ipfs pin add #{cid}`)
28 | - set `status` to `pinned`
29 | - if an error occurs then set `status` to `failed`
30 |
31 | The spec says that a successful update results in a 202 (`:accepted`) response code with body of both the pin and it's status is then returned as JSON, rendered using the same jbuilder template as `pins#show`.
32 |
33 | In rails the default is 200, as it appears that the spec assumes the ipfs actions will be happening in a job queue after the request has completed, although most http clients will accept and 2XX response as successful so this may not matter to much.
34 |
35 | ### [List Pins](https://ipfs.github.io/pinning-services-api-spec/#tag/pins/paths/~1pins/get)
36 |
37 | GET request to /pins go to `pins#index` with optional url query params to filter responses. Default of 10 records per page (max `1000`), always ordered by newest first (`created_at DESC`).
38 |
39 | Pagination is not the usual `?page=1` but instead the client is expected to work out the next page by passing the `created` field of the last record in a response as `before`, so order must always be the by creation date for pagination to work. (An alternative option here would be `offset` which would allow pagination and ordering at the same time).
40 |
41 | `status` can be one or more of the four allowed statuses (`queued`, `pinning`, `pinned`, `failed`), comma separated and if `status` is not provided (or invalid), then it defaults to `pinned`, this are passed straight to ActiveRecord to filter on: `where(status: statuses)`.
42 |
43 | One or more CID can be optionally passed, comma separated to `cid`, as they are case sensitive we can pass the array directly to ActiveRecord to filter on: `where(cid: cids)`.
44 |
45 | Clients can optionally filter results by `name`, which can be an exact or partial case sensitive match, we use a case insensitive `ilike` query in postgresql for this: `where('name ilike ?', "%#{name}%")`.
46 |
47 | Clients can optionally filter results by a key+value pair from the `meta` field, which is provided as json in the query parameter. Because we store `meta` in a `jsonb` field in postgresql, we can use the provided SQL function: `where("meta->>? = ?", meta.key, meta.value)`. The spec is not clear on if the JSON stored in `meta` can be nested, we allow nested JSON to be stored on creation but the query may not work as expected if nested JSON is passed when filtering.
48 |
49 | `before` and `after` allow optional filtering by `created_at`, we can pass the date through to postgresql: `where('created_at < ?', before)` and `where('created_at > ?', after)` if present.
50 |
51 | `count` is the total number of records available for the given filters, ignoring the `limit` we we make a separate SQL query for that before loading the limited `Pin` records, if `count` is zero we can skip the second SQL query and return an empty array.
52 |
53 | The response of an array of both the pin and it's status is then returned as JSON, rendered using the same jbuilder partial as #show along with `count`.
54 |
55 | This endpoint doesn't hit the IPFS node and would make a good candidate for caching.
56 |
57 | ### [Get Pin](https://ipfs.github.io/pinning-services-api-spec/#tag/pins/paths/~1pins~1{requestid}/get)
58 |
59 | GET request to /pins/{id} go to `pins#show` which renders `_pin_status.json.jbuilder` or returns 404 if the pin doesn't exist.
60 |
61 | This endpoint doesn't hit the IPFS node and would make a good candidate for caching.
62 |
63 | ### [Replace Pin](https://ipfs.github.io/pinning-services-api-spec/#tag/pins/paths/~1pins~1{requestid}/post)
64 |
65 | POST request to /pins/{id} go to `pins#update`, unlike the standard rails pattern of using PATCH (or PUT in older versions of rails) for updates so we need to specify an extra route, we also leave the regular PATCH route pointing to the same place but it could be disabled for completeness.
66 |
67 | This endpoint accepts the same request body as `pins#create`, rails will automatically parse the body as json and convert into params. Although the spec does not nest the object in a `pin` object like the ActiveRecord pattern, the keys do map nicely onto fields on our `Pin` database table.
68 |
69 | Unlike in standard CRUD rails, the spec says that rather than updating the pin of the given `id`, a new pin should be created and the old pin removed afterwards.
70 |
71 | > The user can modify an existing pin object via POST /pins/{id}. The new pin object id is returned in the PinStatus response. The old pin object is deleted automatically.
72 |
73 | The response should return JSON for the newly created pin, not the pin of the given `id`.
74 |
75 | It is unclear from the spec what should happen if the CID is the same as on the pin of the given `id`, for now we play it safe and remove the existing pin with `ipfs pin rm #{cid}` and recreate it with `ipfs pin add #{cid}` but we may be able to skip that step and just update the other fields on the database record.
76 |
77 | It is also unclear when the pinning on the new CID fails, should the old pin still be deleted.
78 |
79 | There is also a command in IPFS to update pins (`ipfs pin update #{old-cid} #{new-cid}`) which may be useful for here.
80 |
81 | Similar to in `pins#create`, before saving the `Pin` to the database, we also set the address of our local IPFS node in the `delegates` field, fetched using `ipfs id` and then attempt to connect to each `origin` provided (`ipfs swarm connect #{origin address}`)
82 |
83 | The spec says that a successful update results in a 202 (`:accepted`) response code with body of both the pin and it's status is then returned as JSON, rendered using the same jbuilder template as `pins#show`.
84 |
85 | In rails the default is 200, as it appears that the spec assumes the ipfs actions will be happening in a job queue after the request has completed, although most http clients will accept and 2XX response as successful so this may not matter to much.
86 |
87 | ### [Delete Pin](https://ipfs.github.io/pinning-services-api-spec/#tag/pins/paths/~1pins~1{requestid}/delete)
88 |
89 | DELETE request to /pins/{id} go to `pins#destroy`, just like regular CRUD rails, we look up the `Pin` by `id` (404ing if not found), unpin the `cid` on the IPFS node (`ipfs pin rm #{cid}`) and then delete the record.
90 |
91 | Successful deletion results in a 202 (`:accepted`) response with no body.
92 |
93 | In rails the default is 200 (204 is also standard when there is no response body), as it appears that the spec assumes the ipfs actions will be happening in a job queue after the request has completed, although most http clients will accept and 2XX response as successful so this may not matter to much.
94 |
95 | ## TODO
96 |
97 | - basic unit tests for pins model
98 | - `info` method on model
99 | - status_details
100 | - dag_size
101 | - raw_size
102 | - pinned_until
103 | - basic pinning client for testing
104 | - only unpin cid if this is the only pin with that CID
105 | - method for expiring pins after a certain amount of time (does it deleted it or update status?)
106 |
107 | ## Notes
108 |
109 | - Does NOT support pinning IPNS names at the moment
110 | - Pagination is manual for the client, no need for pagination library or headers in server
111 |
112 | ## Contributing
113 |
114 | Contributions are welcome! This repository is part of the IPFS project and therefore governed by our [contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
115 |
116 | ## License
117 |
118 | [SPDX-License-Identifier: Apache-2.0 OR MIT](LICENSE.md)
119 |
--------------------------------------------------------------------------------