Compare commits
6 Commits
0.0.1
...
a066b73f2d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a066b73f2d | ||
|
|
5e05567309 | ||
|
|
b59ac53e4b | ||
|
|
7f1dc01247 | ||
|
|
0c37362a8a | ||
|
|
5e153bfb86 |
@@ -1,5 +1,9 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.0] - 2025-01-21
|
||||||
|
|
||||||
|
- Rename to from Picop to Picopackage
|
||||||
|
|
||||||
## [0.1.0] - 2025-01-19
|
## [0.1.0] - 2025-01-19
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
picop (0.1.0)
|
picopackage (0.2.0)
|
||||||
digest
|
digest
|
||||||
open-uri (~> 0.5)
|
open-uri (~> 0.5)
|
||||||
yaml (~> 0.4)
|
yaml (~> 0.4)
|
||||||
@@ -70,7 +70,7 @@ PLATFORMS
|
|||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
debug
|
debug
|
||||||
minitest (~> 5.16)
|
minitest (~> 5.16)
|
||||||
picop!
|
picopackage!
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
rubocop (~> 1.21)
|
rubocop (~> 1.21)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Picop
|
# Picopackage
|
||||||
|
|
||||||
TODO: Delete this and the text below, and describe your gem
|
TODO: Delete this and the text below, and describe your gem
|
||||||
|
|
||||||
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/picop`. To experiment with that code, run `bin/console` for an interactive prompt.
|
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/picopackge`. To experiment with that code, run `bin/console` for an interactive prompt.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
lib_path = File.expand_path('../lib', __dir__)
|
lib_path = File.expand_path('../lib', __dir__)
|
||||||
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
||||||
|
|
||||||
require 'picop'
|
require 'picopackage'
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Picop::CLI.run(ARGV)
|
Picopackage::CLI.run(ARGV)
|
||||||
rescue => e
|
rescue => e
|
||||||
warn "Error: #{e.message}"
|
warn "Error: #{e.message}"
|
||||||
warn e.backtrace if ENV['DEBUG']
|
warn e.backtrace if ENV['DEBUG']
|
||||||
11
lib/picop.rb
11
lib/picop.rb
@@ -1,11 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative "picop/version"
|
|
||||||
require_relative "picop/source_file"
|
|
||||||
require_relative "picop/scanner"
|
|
||||||
require_relative "picop/cli"
|
|
||||||
|
|
||||||
module Picop
|
|
||||||
class Error < StandardError; end
|
|
||||||
# Your code goes here...
|
|
||||||
end
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
require "optparse"
|
|
||||||
|
|
||||||
module Picop
|
|
||||||
class CLI
|
|
||||||
def self.run(argv = ARGV)
|
|
||||||
command = argv.shift
|
|
||||||
case command
|
|
||||||
when 'scan'
|
|
||||||
options = {}
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = "Usage: picop scan [options] DIRECTORY"
|
|
||||||
opts.on('-v', '--verbose', 'Run verbosely') { |v| options[:verbose] = v }
|
|
||||||
end.parse!(argv)
|
|
||||||
|
|
||||||
dir = argv.first || '.'
|
|
||||||
Picop::Scanner.scan(dir, options)
|
|
||||||
|
|
||||||
when 'sign'
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = "Usage: picop sign FILE"
|
|
||||||
end.parse!(argv)
|
|
||||||
|
|
||||||
file = argv.first
|
|
||||||
Picop::SourceFile.new(file).sign
|
|
||||||
|
|
||||||
when 'checksum'
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = "Usage: picop checksum FILE"
|
|
||||||
end.parse!(argv)
|
|
||||||
file = argv.first
|
|
||||||
puts Picop::SourceFile.new(file).checksum
|
|
||||||
|
|
||||||
when 'verify'
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = "Usage: picop sign FILE"
|
|
||||||
end.parse!(argv)
|
|
||||||
|
|
||||||
path = argv.first
|
|
||||||
source = SourceFile.new(path)
|
|
||||||
|
|
||||||
if source.metadata['content_checksum'].nil?
|
|
||||||
puts "⚠️ No checksum found in #{path}"
|
|
||||||
puts "Run 'picop sign #{path}' to add one"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
unless source.verify
|
|
||||||
puts "❌ Checksum verification failed for #{path}"
|
|
||||||
puts "Expected: #{source.metadata['content_checksum']}"
|
|
||||||
puts "Got: #{source.checksum}"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "✅ #{path} verified successfully"
|
|
||||||
|
|
||||||
when 'show'
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = "Usage: picop show FILE|DIRECTORY"
|
|
||||||
end.parse!(argv)
|
|
||||||
|
|
||||||
path = argv.first
|
|
||||||
Picop.show(path)
|
|
||||||
|
|
||||||
when 'update'
|
|
||||||
options = {}
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = "Usage: picop update [options] FILE"
|
|
||||||
opts.on('-f', '--force', 'Force update') { |f| options[:force] = f }
|
|
||||||
end.parse!(argv)
|
|
||||||
|
|
||||||
file = argv.first
|
|
||||||
Picop.update(file, options)
|
|
||||||
|
|
||||||
else
|
|
||||||
puts "Unknown command: #{command}"
|
|
||||||
puts "Available commands: scan, sign, show, update"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
rescue OptionParser::InvalidOption => e
|
|
||||||
puts e.message
|
|
||||||
exit 1
|
|
||||||
rescue => e
|
|
||||||
puts "Error: #{e.message}"
|
|
||||||
puts e.backtrace if ENV['DEBUG']
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
require "yaml"
|
|
||||||
require "digest"
|
|
||||||
|
|
||||||
module Picop
|
|
||||||
class SourceFile
|
|
||||||
attr_reader :file_path, :content
|
|
||||||
|
|
||||||
METADATA_PATTERN = /^\s*#\s*@META_START\n(.*?)^\s*#\s*@META_END/m
|
|
||||||
|
|
||||||
def initialize(file_path)
|
|
||||||
@file_path = file_path
|
|
||||||
@content = File.read(file_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
def metadata
|
|
||||||
@metadata ||= begin
|
|
||||||
return {} unless content =~ METADATA_PATTERN
|
|
||||||
|
|
||||||
yaml_content = $1.lines.map do |line|
|
|
||||||
line.sub(/^\s*#\s?/, '').rstrip
|
|
||||||
end.join("\n")
|
|
||||||
|
|
||||||
YAML.safe_load(yaml_content)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_content = content.sub(/^\s*#\s*@META_START\n.*?^\s*#\s*@META_END\n*/m, '')
|
|
||||||
|
|
||||||
def checksum = "sha256:#{Digest::SHA256.hexdigest(file_content)}"
|
|
||||||
|
|
||||||
def show = puts(metadata.merge({checksum: checksum}))
|
|
||||||
|
|
||||||
def save = File.write(file_path, content)
|
|
||||||
|
|
||||||
def add_metadata(metadata_hash)
|
|
||||||
yaml_content = metadata_hash.to_yaml.strip
|
|
||||||
metadata_block = [
|
|
||||||
"# @META_START",
|
|
||||||
yaml_content.lines.map { |line| "# #{line}" }.join,
|
|
||||||
"# @META_END"
|
|
||||||
].join("\n")
|
|
||||||
|
|
||||||
if content =~ METADATA_PATTERN
|
|
||||||
@content = content.sub(METADATA_PATTERN, metadata_block)
|
|
||||||
else
|
|
||||||
@content = [metadata_block, content].join("\n\n")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def sign
|
|
||||||
hash = checksum
|
|
||||||
meta = metadata || {}
|
|
||||||
return puts "File already signed" if meta['content_checksum'] == hash
|
|
||||||
|
|
||||||
meta['content_checksum'] = "#{hash}"
|
|
||||||
add_metadata(meta)
|
|
||||||
save
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify
|
|
||||||
return false unless metadata.key? 'content_checksum'
|
|
||||||
checksum == metadata['content_checksum']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Picop
|
|
||||||
VERSION = "0.1.0"
|
|
||||||
end
|
|
||||||
16
lib/picopackage.rb
Normal file
16
lib/picopackage.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "picopackage/version"
|
||||||
|
require_relative "picopackage/http_fetcher"
|
||||||
|
require_relative "picopackage/provider"
|
||||||
|
require_relative "picopackage/source_file"
|
||||||
|
require_relative "picopackage/scanner"
|
||||||
|
require_relative "picopackage/fetch"
|
||||||
|
require_relative "picopackage/cli"
|
||||||
|
|
||||||
|
module Picopackage
|
||||||
|
class Error < StandardError; end
|
||||||
|
class FileTooLargeError < StandardError; end
|
||||||
|
class FetchError < StandardError; end
|
||||||
|
class LocalModificationError < StandardError; end
|
||||||
|
end
|
||||||
120
lib/picopackage/cli.rb
Normal file
120
lib/picopackage/cli.rb
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
require "optparse"
|
||||||
|
|
||||||
|
module Picopackage
|
||||||
|
class CLI
|
||||||
|
def self.run(argv = ARGV)
|
||||||
|
command = argv.shift
|
||||||
|
case command
|
||||||
|
when 'scan'
|
||||||
|
options = {}
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg scan [options] DIRECTORY"
|
||||||
|
# opts.on('-v', '--verbose', 'Run verbosely') { |v| options[:verbose] = v }
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
dir = argv.first || '.'
|
||||||
|
Picopackage::Scanner.scan(dir).each {|f| puts f.file_path }
|
||||||
|
|
||||||
|
when 'digest'
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg digest FILE"
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
file = argv.first
|
||||||
|
Picopackage::SourceFile.from_file(file).digest!
|
||||||
|
|
||||||
|
when 'checksum'
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg checksum FILE"
|
||||||
|
end.parse!(argv)
|
||||||
|
file = argv.first
|
||||||
|
puts Picopackage::SourceFile.from_file(file).checksum
|
||||||
|
|
||||||
|
when 'verify'
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg sign FILE"
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
path = argv.first
|
||||||
|
source = SourceFile.from_file(path)
|
||||||
|
|
||||||
|
if source.metadata['content_checksum'].nil?
|
||||||
|
puts "⚠️ No checksum found in #{path}"
|
||||||
|
puts "Run 'ppkg sign #{path}' to add one"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
unless source.verify
|
||||||
|
puts "❌ Checksum verification failed for #{path}"
|
||||||
|
puts "Expected: #{source.metadata['content_checksum']}"
|
||||||
|
puts "Got: #{source.checksum}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "✅ #{path} verified successfully"
|
||||||
|
|
||||||
|
when 'inspect'
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg inspect FILE|DIRECTORY"
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
path = argv.first
|
||||||
|
Picopackage::SourceFile.from_file(path).inspect
|
||||||
|
|
||||||
|
when 'fetch'
|
||||||
|
options = { force: false }
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg fetch [options] URI [PATH]"
|
||||||
|
opts.on('-f', '--force', 'Force fetch') { |f| options[:force] = f }
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
url = argv.shift
|
||||||
|
path = argv.shift || '.' # use '.' if no path provided
|
||||||
|
|
||||||
|
if url.nil?
|
||||||
|
puts "Error: URI is required"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
Fetch.fetch(url, path, force: options[:force])
|
||||||
|
rescue LocalModificationError => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
rescue => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
when 'update'
|
||||||
|
options = { force: false }
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: ppkg update [options] FILE"
|
||||||
|
opts.on('-f', '--force', 'Force update') { |f| options[:force] = f }
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
file = argv.first
|
||||||
|
source_file = SourceFile.from_file(file)
|
||||||
|
begin
|
||||||
|
Fetch.fetch(source_file.url, File.dirname(file), force: options[:force])
|
||||||
|
rescue LocalModificationError => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
rescue => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
puts "Unknown command: #{command}"
|
||||||
|
puts "Available commands: scan, sign, inspect, update"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
rescue OptionParser::InvalidOption => e
|
||||||
|
puts e.message
|
||||||
|
exit 1
|
||||||
|
rescue => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
puts e.backtrace if ENV['DEBUG']
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
105
lib/picopackage/fetch.rb
Normal file
105
lib/picopackage/fetch.rb
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
require 'net/http'
|
||||||
|
require 'fileutils'
|
||||||
|
require 'tempfile'
|
||||||
|
require 'json'
|
||||||
|
require 'debug'
|
||||||
|
|
||||||
|
module Picopackage
|
||||||
|
class Fetch
|
||||||
|
def self.fetch(url, destination, force: false)
|
||||||
|
raise ArgumentError, "Destination directory does not exist: #{destination}" unless Dir.exist?(destination)
|
||||||
|
|
||||||
|
provider = Provider.for(url)
|
||||||
|
source_file = provider.source_file
|
||||||
|
|
||||||
|
file_path = File.join(destination, source_file.filename)
|
||||||
|
|
||||||
|
if File.exist?(file_path) && force
|
||||||
|
source_file.save(destination)
|
||||||
|
elsif File.exist?(file_path)
|
||||||
|
local_source_file = SourceFile.from_file(file_path)
|
||||||
|
status = Status.compare(local_source_file, source_file)
|
||||||
|
|
||||||
|
if force
|
||||||
|
source_file.save(destination)
|
||||||
|
elsif status.modified?
|
||||||
|
raise LocalModificationError, "#{status.message}. Use -f or --force to overwrite local version"
|
||||||
|
elsif status.outdated?
|
||||||
|
puts "Updated from #{local_source_file.version} to #{source_file.version}"
|
||||||
|
source_file.save(destination)
|
||||||
|
elsif status.up_to_date?
|
||||||
|
puts status.message
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
source_file.save(destination)
|
||||||
|
if source_file.imported?
|
||||||
|
source_file.digest!
|
||||||
|
puts "Picopackage created for #{source_file.filename}"
|
||||||
|
else
|
||||||
|
puts "Picopackage downloaded to #{file_path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
provider.source_file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Status
|
||||||
|
attr_reader :state, :local_version, :remote_version
|
||||||
|
|
||||||
|
def self.compare(local_source_file, remote_source_file)
|
||||||
|
return new(:outdated) if local_source_file.metadata.nil? || remote_source_file.metadata.nil?
|
||||||
|
|
||||||
|
local_version = local_source_file.metadata["version"]
|
||||||
|
remote_version = remote_source_file.metadata["version"]
|
||||||
|
|
||||||
|
if local_version == remote_version
|
||||||
|
if local_source_file.modified?
|
||||||
|
new(:modified, local_version:)
|
||||||
|
else
|
||||||
|
new(:up_to_date, local_version:)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
new(:outdated,
|
||||||
|
local_version:,
|
||||||
|
remote_version:,
|
||||||
|
modified: local_source_file.modified?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(state, local_version: nil, remote_version: nil, modified: false)
|
||||||
|
@state = state
|
||||||
|
@local_version = local_version
|
||||||
|
@remote_version = remote_version
|
||||||
|
@modified = modified
|
||||||
|
end
|
||||||
|
|
||||||
|
def modified?
|
||||||
|
@modified || @state == :modified
|
||||||
|
end
|
||||||
|
|
||||||
|
def up_to_date?
|
||||||
|
@state == :up_to_date
|
||||||
|
end
|
||||||
|
|
||||||
|
def outdated?
|
||||||
|
@state == :outdated
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
case state
|
||||||
|
when :up_to_date
|
||||||
|
"Picopackage is up to date"
|
||||||
|
when :outdated
|
||||||
|
if modified?
|
||||||
|
"Local Picopackage (v#{local_version}) has modifications but remote version (v#{remote_version}) is available"
|
||||||
|
else
|
||||||
|
"Local Picopackage (v#{local_version}) is outdated. Remote version: v#{remote_version}"
|
||||||
|
end
|
||||||
|
when :modified
|
||||||
|
"Local Picopackage has been modified from original version (v#{local_version})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
112
lib/picopackage/provider.rb
Normal file
112
lib/picopackage/provider.rb
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
module Picopackage
|
||||||
|
class Provider
|
||||||
|
def self.for(url)
|
||||||
|
PROVIDERS.each do |provider|
|
||||||
|
case provider.handles_url?(url)
|
||||||
|
when false
|
||||||
|
next
|
||||||
|
when true
|
||||||
|
return provider.new(url)
|
||||||
|
when :maybe
|
||||||
|
instance = provider.new(url)
|
||||||
|
return instance if instance.handles_body?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nil # Return nil if no provider found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Base class for fetching content from a URL
|
||||||
|
# The variable `body` will contain the content retrieved from the URL
|
||||||
|
# The variable `content` will contain both and code + metadata - this would be writen to a file.
|
||||||
|
# The variable `code` will contain the code extracted from `content`
|
||||||
|
# The variable `metadata` will contain the metadata extracted from `content`
|
||||||
|
|
||||||
|
# Job of the Provider class is to fetch the body from the URL, and then extract the content and the filename from the body
|
||||||
|
# The SourceFile class will then take the body and split it into code and metadata
|
||||||
|
|
||||||
|
class DefaultProvider
|
||||||
|
MAX_SIZE = 1024 * 1024
|
||||||
|
TIMEOUT = 10
|
||||||
|
attr_reader :url, :source_file
|
||||||
|
|
||||||
|
def self.handles_url?(url) = :maybe
|
||||||
|
|
||||||
|
def initialize(url)
|
||||||
|
@url = transform_url(url)
|
||||||
|
@uri = URI(@url)
|
||||||
|
@body = nil
|
||||||
|
@content = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def body = @body ||= fetch
|
||||||
|
def json_body = @json_body ||= JSON.parse(body)
|
||||||
|
def transform_url(url) = url
|
||||||
|
|
||||||
|
def fetch
|
||||||
|
begin
|
||||||
|
Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == 'https', read_timeout: TIMEOUT, open_timeout: TIMEOUT) do |http|
|
||||||
|
http.request_get(@uri.path) do |response|
|
||||||
|
raise "Unexpected response: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
||||||
|
|
||||||
|
@body = String.new(capacity: MAX_SIZE)
|
||||||
|
response.read_body do |chunk|
|
||||||
|
if @body.bytesize + chunk.bytesize > MAX_SIZE
|
||||||
|
raise FileTooLargeError, "Response would exceed #{MAX_SIZE} bytes"
|
||||||
|
end
|
||||||
|
@body << chunk
|
||||||
|
end
|
||||||
|
@body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@body
|
||||||
|
end
|
||||||
|
|
||||||
|
def handles_body?
|
||||||
|
true
|
||||||
|
rescue FileTooLargeError, Net::HTTPError, RuntimeError => e
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Implement in subclass - this come from the `body`.
|
||||||
|
# Spliting content into code and metadata is the job of the SourceFile class
|
||||||
|
def content = body
|
||||||
|
|
||||||
|
# Implement in subclass - this should return the filename extracted from the body - if it exists, but not from the metadata
|
||||||
|
def filename = File.basename @url
|
||||||
|
|
||||||
|
def source_file
|
||||||
|
@source_file ||= SourceFile.from_content(content, metadata: {'filename' => filename, 'url' => url, 'version' => '0.0.1'})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class GithubGistProvider < DefaultProvider
|
||||||
|
def self.handles_url?(url) = url.match?(%r{gist\.github\.com})
|
||||||
|
def content = json_body["files"].values.first["content"]
|
||||||
|
def filename = json_body["files"].values.first["filename"]
|
||||||
|
def transform_url(url)
|
||||||
|
gist_id = url[/gist\.github\.com\/[^\/]+\/([a-f0-9]+)/, 1]
|
||||||
|
"https://api.github.com/gists/#{gist_id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class OpenGistProvider < DefaultProvider
|
||||||
|
def handles_url?(url) = :maybe
|
||||||
|
def transform_url(url) = "#{url}.json"
|
||||||
|
def content = json_body.dig("files",0, "content")
|
||||||
|
def filename = json_body.dig("files",0, "filename")
|
||||||
|
def handles_body?
|
||||||
|
content && filename
|
||||||
|
rescue FileTooLargeError, Net::HTTPError, RuntimeError => e
|
||||||
|
false
|
||||||
|
end
|
||||||
|
# If we successfully fetch the body, and the body contains content and a filename, then we can handle the body
|
||||||
|
end
|
||||||
|
|
||||||
|
PROVIDERS = [
|
||||||
|
GithubGistProvider,
|
||||||
|
OpenGistProvider,
|
||||||
|
DefaultProvider
|
||||||
|
].freeze
|
||||||
|
end
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module Picop
|
module Picopackage
|
||||||
module Scanner
|
module Scanner
|
||||||
def self.scan(directory, pattern: "**/*")
|
def self.scan(directory, pattern: "**/*")
|
||||||
Dir.glob(File.join(directory, pattern)).select do |file|
|
Dir.glob(File.join(directory, pattern)).select do |file|
|
||||||
120
lib/picopackage/source_file.rb
Normal file
120
lib/picopackage/source_file.rb
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
require "yaml"
|
||||||
|
require "digest"
|
||||||
|
|
||||||
|
module Picopackage
|
||||||
|
class SourceFile
|
||||||
|
attr_reader :content, :metadata, :code, :original_path
|
||||||
|
|
||||||
|
METADATA_PATTERN = /^\n*#\s*@PICOPACKAGE_START\n(.*?)^\s*#\s*@PICOPACKAGE_END\s*$/m
|
||||||
|
|
||||||
|
def self.from_file(file_path) = new(content: File.read(file_path), original_path: file_path)
|
||||||
|
|
||||||
|
def self.from_content(content, metadata: {})
|
||||||
|
instance = new(content: content)
|
||||||
|
instance.imported! if instance.metadata.empty?
|
||||||
|
|
||||||
|
updated_metadata = metadata.merge(instance.metadata)
|
||||||
|
|
||||||
|
## For new Picopackages, we should add metadata and checksum
|
||||||
|
instance.update_metadata(updated_metadata)
|
||||||
|
|
||||||
|
instance
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(content:, original_path: nil)
|
||||||
|
@original_path = original_path
|
||||||
|
|
||||||
|
@content = content
|
||||||
|
@metadata = extract_metadata
|
||||||
|
@code = extract_code
|
||||||
|
end
|
||||||
|
|
||||||
|
def imported! = @imported = true
|
||||||
|
|
||||||
|
def imported? = @imported ||= false
|
||||||
|
|
||||||
|
def content = @content
|
||||||
|
|
||||||
|
def url = @metadata['url']
|
||||||
|
|
||||||
|
def filename = @metadata['filename']
|
||||||
|
|
||||||
|
def version = @metadata['version'] || '0.0.1'
|
||||||
|
|
||||||
|
def checksum = "sha256:#{Digest::SHA256.hexdigest(code)}"
|
||||||
|
|
||||||
|
def inspect_metadata = puts JSON.pretty_generate(@metadata)
|
||||||
|
|
||||||
|
def save(destination = nil)
|
||||||
|
path = determine_save_path(destination)
|
||||||
|
File.write(path, content)
|
||||||
|
path
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_code = content.sub(METADATA_PATTERN, '')
|
||||||
|
|
||||||
|
def extract_metadata
|
||||||
|
return {} unless content =~ METADATA_PATTERN
|
||||||
|
|
||||||
|
yaml_content = $1.lines.map do |line|
|
||||||
|
line.sub(/^\s*#\s?/, '').rstrip
|
||||||
|
end.join("\n")
|
||||||
|
|
||||||
|
YAML.safe_load(yaml_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_metadata(metadata_hash)
|
||||||
|
@metadata = metadata_hash
|
||||||
|
@content = generate_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest!
|
||||||
|
hash = checksum
|
||||||
|
return puts "File already has a checksum" if metadata['content_checksum'] == hash
|
||||||
|
|
||||||
|
new_metadata = metadata.merge('content_checksum' => hash)
|
||||||
|
update_metadata(new_metadata)
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify
|
||||||
|
return false unless metadata.key? 'content_checksum'
|
||||||
|
checksum == metadata['content_checksum']
|
||||||
|
end
|
||||||
|
|
||||||
|
def modified? = !verify
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_content
|
||||||
|
metadata_block = generate_metadata
|
||||||
|
if content =~ METADATA_PATTERN
|
||||||
|
content.sub(METADATA_PATTERN, "\n#{metadata_block}")
|
||||||
|
else
|
||||||
|
[content.rstrip, "\n#{metadata_block}"].join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This will need a comment style one day, to work with other languages
|
||||||
|
def generate_metadata
|
||||||
|
yaml_content = @metadata.to_yaml.strip
|
||||||
|
[
|
||||||
|
"# @PICOPACKAGE_START",
|
||||||
|
yaml_content.lines.map { |line| "# #{line}" }.join,
|
||||||
|
"# @PICOPACKAGE_END",
|
||||||
|
""
|
||||||
|
].join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def determine_save_path(destination)
|
||||||
|
if destination.nil?
|
||||||
|
@original_path || filename || raise("No filename available")
|
||||||
|
elsif File.directory?(destination)
|
||||||
|
File.join(destination, filename || File.basename(@original_path))
|
||||||
|
else
|
||||||
|
destination
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
5
lib/picopackage/version.rb
Normal file
5
lib/picopackage/version.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Picopackage
|
||||||
|
VERSION = "0.2.0"
|
||||||
|
end
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative "lib/picop/version"
|
require_relative "lib/picopackage/version"
|
||||||
|
|
||||||
Gem::Specification.new do |spec|
|
Gem::Specification.new do |spec|
|
||||||
spec.name = "picop"
|
spec.name = "picopackage"
|
||||||
spec.version = Picop::VERSION
|
spec.version = Picopackage::VERSION
|
||||||
spec.authors = ["Dan Milne"]
|
spec.authors = ["Dan Milne"]
|
||||||
spec.email = ["d@nmilne.com"]
|
spec.email = ["d@nmilne.com"]
|
||||||
|
|
||||||
spec.summary = "TODO: Write a short summary, because RubyGems requires one."
|
spec.summary = "Picopackage Tool."
|
||||||
spec.description = "TODO: Write a longer description or delete this line."
|
spec.description = "Picopackage Tool for managing Picopackages."
|
||||||
spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
spec.homepage = "https://picopackage.org"
|
||||||
spec.license = "MIT"
|
spec.license = "MIT"
|
||||||
spec.required_ruby_version = ">= 3.1.0"
|
spec.required_ruby_version = ">= 3.1.0"
|
||||||
|
|
||||||
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
#spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
||||||
|
|
||||||
spec.metadata["homepage_uri"] = spec.homepage
|
#spec.metadata["homepage_uri"] = spec.homepage
|
||||||
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
#spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
||||||
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
#spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
||||||
|
|
||||||
# Specify which files should be added to the gem when it is released.
|
# Specify which files should be added to the gem when it is released.
|
||||||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
||||||
require "picop"
|
require "picopackage"
|
||||||
|
|
||||||
require "minitest/autorun"
|
require "minitest/autorun"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class TestPicop < Minitest::Test
|
class TestPicopackage < Minitest::Test
|
||||||
def test_that_it_has_a_version_number
|
def test_that_it_has_a_version_number
|
||||||
refute_nil ::Picop::VERSION
|
refute_nil ::Picopackage::VERSION
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_it_does_something_useful
|
def test_it_does_something_useful
|
||||||
Reference in New Issue
Block a user