5 Commits

11 changed files with 119 additions and 84 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

16
lib/picopackage.rb Normal file
View 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

View File

@@ -15,13 +15,13 @@ module Picopackage
dir = argv.first || '.' dir = argv.first || '.'
Picopackage::Scanner.scan(dir).each {|f| puts f.file_path } Picopackage::Scanner.scan(dir).each {|f| puts f.file_path }
when 'sign' when 'digest'
OptionParser.new do |opts| OptionParser.new do |opts|
opts.banner = "Usage: ppkg sign FILE" opts.banner = "Usage: ppkg digest FILE"
end.parse!(argv) end.parse!(argv)
file = argv.first file = argv.first
Picopackage::SourceFile.from_file(file).sign Picopackage::SourceFile.from_file(file).digest!
when 'checksum' when 'checksum'
OptionParser.new do |opts| OptionParser.new do |opts|
@@ -77,14 +77,30 @@ module Picopackage
end end
begin begin
source_file = Fetch.fetch(url, path, force: options[:force]) 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 rescue LocalModificationError => e
puts "Error: #{e.message}" puts "Error: #{e.message}"
rescue => e rescue => e
puts "Error: #{e.message}" puts "Error: #{e.message}"
exit 1 exit 1
# Optionally retry with force
# source_file = Fetch.fetch(url, destination, force: true)
end end
else else

View File

@@ -4,33 +4,41 @@ require 'tempfile'
require 'json' require 'json'
require 'debug' require 'debug'
module Picop module Picopackage
class Fetch class Fetch
def self.fetch(url, destination, force: false) def self.fetch(url, destination, force: false)
raise ArgumentError, "Destination directory does not exist: #{destination}" unless Dir.exist?(destination) raise ArgumentError, "Destination directory does not exist: #{destination}" unless Dir.exist?(destination)
debugger
provider = Provider.for(url) provider = Provider.for(url)
file_path = File.join(destination, provider.source_file.filename) source_file = provider.source_file
debugger
file_path = File.join(destination, source_file.filename)
if File.exist?(file_path) && force if File.exist?(file_path) && force
provider.source_file.save(destination) source_file.save(destination)
elsif File.exist?(file_path) elsif File.exist?(file_path)
local_source_file = SourceFile.from_file(file_path) local_source_file = SourceFile.from_file(file_path)
status = Status.compare(local_source_file, provider.source_file) status = Status.compare(local_source_file, source_file)
if force if force
provider.source_file.save(destination) source_file.save(destination)
elsif status.modified? elsif status.modified?
raise LocalModificationError, "#{status.message}. Use -f or --force to overwrite local version" raise LocalModificationError, "#{status.message}. Use -f or --force to overwrite local version"
elsif status.outdated? elsif status.outdated?
puts "Updated from #{local_source_file.version} to #{provider.source_file.version}" puts "Updated from #{local_source_file.version} to #{source_file.version}"
provider.source_file.save(destination) source_file.save(destination)
elsif status.up_to_date? elsif status.up_to_date?
puts status.message puts status.message
end end
else else
provider.source_file.save(destination) 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 end
provider.source_file provider.source_file
end end
@@ -82,15 +90,15 @@ debugger
def message def message
case state case state
when :up_to_date when :up_to_date
"File is up to date" "Picopackage is up to date"
when :outdated when :outdated
if modified? if modified?
"Local file (v#{local_version}) has modifications but remote version (v#{remote_version}) is available" "Local Picopackage (v#{local_version}) has modifications but remote version (v#{remote_version}) is available"
else else
"Local file (v#{local_version}) is outdated. Remote version: v#{remote_version}" "Local Picopackage (v#{local_version}) is outdated. Remote version: v#{remote_version}"
end end
when :modified when :modified
"Local file has been modified from original version (v#{local_version})" "Local Picopackage has been modified from original version (v#{local_version})"
end end
end end
end end

View File

@@ -1,4 +1,4 @@
module Picop module Picopackage
class Provider class Provider
def self.for(url) def self.for(url)
PROVIDERS.each do |provider| PROVIDERS.each do |provider|
@@ -28,7 +28,6 @@ module Picop
class DefaultProvider class DefaultProvider
MAX_SIZE = 1024 * 1024 MAX_SIZE = 1024 * 1024
TIMEOUT = 10 TIMEOUT = 10
attr_reader :url, :source_file attr_reader :url, :source_file
def self.handles_url?(url) = :maybe def self.handles_url?(url) = :maybe
@@ -40,9 +39,9 @@ module Picop
@content = nil @content = nil
end end
def transform_url(url) = url
def body = @body ||= fetch def body = @body ||= fetch
def json_body = @json_body ||= JSON.parse(body)
def transform_url(url) = url
def fetch def fetch
begin begin
@@ -66,62 +65,43 @@ module Picop
def handles_body? def handles_body?
true true
rescue FileTooLargeError rescue FileTooLargeError, Net::HTTPError, RuntimeError => e
false false
end end
def content # Implement in subclass - this come from the `body`.
# Implement in subclass - this come from the `body`. Spliting content into code and metadata is the job of the SourceFile class # Spliting content into code and metadata is the job of the SourceFile class
raise NotImplementedError def content = body
end
def filename
# Implement in subclass - this should return the filename extracted from the body - if it exists, but not from the metadata # Implement in subclass - this should return the filename extracted from the body - if it exists, but not from the metadata
raise NotImplementedError def filename = File.basename @url
end
def source_file def source_file
@source_file ||= SourceFile.from_content(content) @source_file ||= SourceFile.from_content(content, metadata: {'filename' => filename, 'url' => url, 'version' => '0.0.1'})
end end
end end
class GithubGistProvider < DefaultProvider class GithubGistProvider < DefaultProvider
def self.handles_url?(url) = url.match?(%r{gist\.github\.com}) 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) def transform_url(url)
gist_id = url[/gist\.github\.com\/[^\/]+\/([a-f0-9]+)/, 1] gist_id = url[/gist\.github\.com\/[^\/]+\/([a-f0-9]+)/, 1]
"https://api.github.com/gists/#{gist_id}" "https://api.github.com/gists/#{gist_id}"
end end
def content
data = JSON.parse(body)
file = data["files"].values.first["content"]
end
def filename
data = JSON.parse(body)
data["files"].values.first["filename"]
end
end end
class OpenGistProvider < DefaultProvider class OpenGistProvider < DefaultProvider
def handles_url?(url) def handles_url?(url) = :maybe
:maybe def transform_url(url) = "#{url}.json"
end def content = json_body.dig("files",0, "content")
def filename = json_body.dig("files",0, "filename")
def transform_url(url) def handles_body?
"#{url}.json" content && filename
end rescue FileTooLargeError, Net::HTTPError, RuntimeError => e
false
def content
data = JSON.parse(body)
@content = data.dig("files",0, "content")
end
def filename
data = JSON.parse(body)
data.dig("files",0, "filename")
end end
# If we successfully fetch the body, and the body contains content and a filename, then we can handle the body
end end
PROVIDERS = [ PROVIDERS = [

View File

@@ -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|

View File

@@ -1,7 +1,7 @@
require "yaml" require "yaml"
require "digest" require "digest"
module Picop module Picopackage
class SourceFile class SourceFile
attr_reader :content, :metadata, :code, :original_path attr_reader :content, :metadata, :code, :original_path
@@ -9,12 +9,15 @@ module Picop
def self.from_file(file_path) = new(content: File.read(file_path), original_path: file_path) def self.from_file(file_path) = new(content: File.read(file_path), original_path: file_path)
def self.from_content(content, filename: nil) def self.from_content(content, metadata: {})
instance = new(content: content) instance = new(content: content)
if filename && !instance.metadata['filename'] instance.imported! if instance.metadata.empty?
metadata = instance.metadata.merge('filename' => filename)
instance.update_metadata(metadata) #TODO: FIX THIS updated_metadata = metadata.merge(instance.metadata)
end
## For new Picopackages, we should add metadata and checksum
instance.update_metadata(updated_metadata)
instance instance
end end
@@ -26,9 +29,17 @@ module Picop
@code = extract_code @code = extract_code
end end
def imported! = @imported = true
def imported? = @imported ||= false
def content = @content
def url = @metadata['url']
def filename = @metadata['filename'] def filename = @metadata['filename']
def version = @metadata['version'] || '0.0.0' def version = @metadata['version'] || '0.0.1'
def checksum = "sha256:#{Digest::SHA256.hexdigest(code)}" def checksum = "sha256:#{Digest::SHA256.hexdigest(code)}"
@@ -57,9 +68,9 @@ module Picop
@content = generate_content @content = generate_content
end end
def sign def digest!
hash = checksum hash = checksum
return puts "File already signed" if metadata['content_checksum'] == hash return puts "File already has a checksum" if metadata['content_checksum'] == hash
new_metadata = metadata.merge('content_checksum' => hash) new_metadata = metadata.merge('content_checksum' => hash)
update_metadata(new_metadata) update_metadata(new_metadata)

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Picop module Picopackage
VERSION = "0.1.0" VERSION = "0.2.0"
end end

View File

@@ -8,17 +8,17 @@ Gem::Specification.new do |spec|
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.