Browse Source

initial commit! ヽ(^o^)丿

tags/v1.0.0
katherine 10 months ago
commit
9511acbce2
7 changed files with 500 additions and 0 deletions
  1. 9
    0
      .gitignore
  2. 21
    0
      LICENSE
  3. 110
    0
      README.md
  4. 9
    0
      shard.yml
  5. 2
    0
      spec/spec_helper.cr
  6. 50
    0
      spec/xdg_basedir_spec.cr
  7. 299
    0
      src/xdg_basedir.cr

+ 9
- 0
.gitignore View File

@@ -0,0 +1,9 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock

+ 21
- 0
LICENSE View File

@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2018 katherine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

+ 110
- 0
README.md View File

@@ -0,0 +1,110 @@
xdg_basedir
===========

This is a simple crystal interface to the XDG Base Directories. It is based on
the XDG Base Directory Specification, the latest version of which (0.7) can be
found [here](https://specifications.freedesktop.org/basedir-spec/0.7/).

The XDG Base Directories Specification is a definition of the directories where
things like a program's configuration files and stored data ought to be written
to and read from, along with the order of precedence to be used when searching
for those kinds of files. If you've ever seen a program that stores its
configurations in the `.config` directory, that program is, at least in part,
following this specification.

This crystal interface to the specification provides methods for simply listing
the base directories, as well as a helper method for easily building file paths
relative to them.


installation
------------

First, add the dependency to your project's `shard.yml` file:

```yaml
dependencies:
xdg_basedir:
github: shmibs/xdg_basedir
```

and then run `shards install`.


usage
-----

Suppose you're writing a program called `program_name`, and you want to read
one of its configuration files, `file_name.conf`. After reading, you want to
perform some operation on the contents of the file, and then write the new
contents back to `file_name.conf`. Using this module, that might look something
like the following:

```crystal
require "xdg_basedir"

# Note: for simplicity's sake, exception handling has been ignored for the
# calls to File.read and File.write

# typically, files in the XDG Base Directories will be first sorted into
# directories, based on the program that uses them. This isn't always the case
# however, and so it is not enforced
read_path = XDGBasedir.full_path("program_name/file_name.conf", :config, :read)

# the specification dictates that base directory locations be determined using
# both the state of the filesystem and the state of certain environment
# variables. it's thus possible that an appropriate base directory won't be
# found, so a nil check is required
if read_path
contents = File.read(read_path)

# ...do something with the contents here...

# write_path here is not necessarily the same as read_path above. full_path
# above will check through the hierarchy of fallback base directories and, if
# it finds the target, return the path into the directory where it was found.
# there is only one base directory for writing, however, and so it is always
# returned here. this means that the first time program_name is run, it might
# read in some system-wide config, write back a user-specific config, and
# then read the user-specific version thereafter
write_path = XDGBasedir.full_path("program_name/file_name.conf", :config,
:write)

# again, nil check necessary...
if write_path
File.write(write_path, contents)
end
end
```

The `full_path` method takes an argument *type*, which was set above to
`:config`. This argument indicates the type of files that are stored in the
base directory that should be selected. There are four possible types:

- `:data` directories are used for storing and retrieving persistent files
across multiple runs of a program.
- `:config` directories are used for storing and retrieving a program's
configuration files.
- `:cache` directories are used for storing non-essential data which may or may
not be retained
- `:runtime` directories are used for storing runtime files (e.g. lock files or
sockets)

Every method defined under `XDGBasedir` takes one of these types as an
argument.

In addition to `full_path`, two lower-level methods are also provided:

- `write_dir`, which returns the single directory where files of a given type
should be written
- `read_dirs`, which returns a hierarchical list of base directories from which
files of a given type should be read

However, these two methods will probably be less useful.


license
-------

This library is licensed under [The MIT
License](https://opensource.org/licenses/MIT).

+ 9
- 0
shard.yml View File

@@ -0,0 +1,9 @@
name: xdg_basedir
version: 1.0.0

authors:
- katherine <shmibs@shmibbles.me>

crystal: 0.27.0

license: MIT

+ 2
- 0
spec/spec_helper.cr View File

@@ -0,0 +1,2 @@
require "spec"
require "../src/xdg_basedir"

+ 50
- 0
spec/xdg_basedir_spec.cr View File

@@ -0,0 +1,50 @@
require "./spec_helper"

# NOTE: these tests are non-exhaustive and may fail, as they depend on the
# state of the filesystem and environment
describe XDGBasedir do

it "finds previously written files using full_path" do
p = XDGBasedir.full_path "xdg_basedir_test_file_1", :data, "w"
unless p
next
end
File.write p, "content"

p = XDGBasedir.full_path "xdg_basedir_test_file_1", :data, "r"
unless p
next
end

s = File.read p
f = File.new p
f.delete

s.should eq("content")
end

it "finds previously written files using dir methods" do
d = XDGBasedir.write_dir :config
unless d
next
end
File.write "#{d}xdg_basedir_test_file_2" , "content"

l = XDGBasedir.read_dirs :config
unless l
next
end

s = File.read "#{l[0]}xdg_basedir_test_file_2"
f = File.new "#{d}xdg_basedir_test_file_2"
f.delete

s.should eq("content")
end

it "fails on bad arguments" do
expect_raises(ArgumentError) { XDGBasedir.write_dir :bad }
expect_raises(ArgumentError) { XDGBasedir.read_dirs :bad }
expect_raises(ArgumentError) { XDGBasedir.full_path "rel_path", :bad }
end
end

+ 299
- 0
src/xdg_basedir.cr View File

@@ -0,0 +1,299 @@
# This module implements an interface to the XDG Base Directories, compliant
# with version 0.7 of the XDG Base Directory Specification.
#
# Provided are methods for locating XDG Base Directories at runtime, as well as
# a helper method for easily building full file paths relative to them.
#
# Each of the operations below can be performed on one of four categories of
# directories, designated via an argument *type* of value `:data`, `:config`,
# `:cache`, or `:runtime`.
#
# - `:data` directories are used for storing and retrieving persistent files
# across multiple runs of a program.
# - `:config` directories are used for storing and retrieving a program's
# configuration files.
# - `:cache` directories are used for storing non-essential data which may or
# may not be retained
# - `:runtime` directories are used for storing runtime files (e.g. lock files
# or sockets)
#
# For more details, please refer to the specification, which is available
# online [here](https://specifications.freedesktop.org/basedir-spec/0.7/).
module XDGBasedir
VERSION = "1.0.0"

# get the base directory into which files of a given *type* should be written
#
# Given a *type* of `:data`, `:config`, `:cache`, or `:runtime`, this method
# returns a single directory into which data of that type should be written,
# or else `nil`, if no appropriate candidate is found.
#
# If a directory is returned, it is guaranteed to always have a terminating
# '/' character, meaning that, when trying to write a file, it is safe to
# directly concatenate the directory and the file path that is to be written
# to within it.
#
# ### Example
#
# dir = XDGBasedir.write_dir :config
# if dir
# File.write "#{dir}created/file.conf", "contents"
# end
#
def self.write_dir(type = :config) : String?
case type
when :data
if ENV["XDG_DATA_HOME"]? && ENV["XDG_DATA_HOME"] != ""
s = "#{ENV["XDG_DATA_HOME"]}/"
else
if ENV["HOME"]?
s = "#{ENV["HOME"]}/.local/share/"
else
return nil
end
end

when :config
if ENV["XDG_CONFIG_HOME"]? && ENV["XDG_CONFIG_HOME"] != ""
s = "#{ENV["XDG_CONFIG_HOME"]}/"
else
if ENV["HOME"]?
s = "#{ENV["HOME"]}/.config/"
else
return nil
end
end

when :cache
if ENV["XDG_CACHE_HOME"]? && ENV["XDG_CACHE_HOME"] != ""
s = "#{ENV["XDG_CACHE_HOME"]}/"
else
if ENV["HOME"]?
s = "#{ENV["HOME"]}/.cache/"
else
return nil
end
end

when :runtime
if ENV["XDG_RUNTIME_DIR"]? && ENV["XDG_RUNTIME_DIR"] != ""
s = "#{ENV["XDG_RUNTIME_DIR"]}/"
else
# runtime dir has no fallback
return nil
end

# runtime dir must necessarily have certain permissions
unless File.directory?(s) && File.info(s).permissions.value == 0o0700
return nil
end

else
raise ArgumentError.new(
"type must be one of: :data, :config, :cache, :runtime"
)
end

# return result with slashes deduplicated. nil check required because the
# compiler can't work out that s will have a value here...
s ? s.gsub(/\/+/, "/") : nil

end

# get a list of base directories from which files of a given *type* can be
# read
#
# Given a type of `:data`, `:config`, `:cache`, or `:runtime`, this method
# returns a string array of directories from which data of that type should
# be read. If no appropriate candidates are found, it instead returns `nil`.
#
# The returned list will be ordered according to precedence. That is, given a
# returned list `l`, accessing a file should first be attempted from within
# `l[0]`, if that fails, be attempted from within `l[1]`, and so on.
#
# If a directory list is returned, those directories are guaranteed to always
# have a terminating '/' character, meaning that, when trying to access a
# file, it is safe to directly concatenate the directory and the file path
# that is to be accessed within it.
#
# ### Example
#
# contents = nil
# dir_list = XDGBasedir.read_dirs :config
#
# if dir_list
# dir_list.each { |dir|
# if File.file? "#{dir}target/file.conf"
# contents = File.read "#{dir}target/file.conf"
# break
# end
# }
# end
#
def self.read_dirs(type = :config) : Array(String)?
case type
when :data
# first entry is the write dir, if it exists
if s = self.write_dir(:data)
l = [s]
else
l = [] of String
end

if ENV["XDG_DATA_DIRS"]? && !/^:*$/.match(ENV["XDG_DATA_DIRS"])
ENV["XDG_DATA_DIRS"].split(":").reject{|s| s == ""}.each{|s| l << s}
else
l << "/usr/local/share/"
l << "/usr/share/"
end

when :config
# first entry is the write dir, if it exists
if s = self.write_dir(:config)
l = [s]
else
l = [] of String
end

if ENV["XDG_CONFIG_DIRS"]? && !/^:*$/.match(ENV["XDG_CONFIG_DIRS"])
ENV["XDG_CONFIG_DIRS"].split(":").reject{|d| d == ""}.each{|s| l << s}
else
l << "/etc/xdg/"
end

when :cache
if ENV["XDG_CACHE_HOME"]? && ENV["XDG_CACHE_HOME"] != ""
l = ["#{ENV["XDG_CACHE_HOME"]}/"]
else
if ENV["HOME"]?
l = ["#{ENV["HOME"]}/.cache/"]
else
return nil
end
end

when :runtime
if ENV["XDG_RUNTIME_DIR"]? && ENV["XDG_RUNTIME_DIR"] != ""
s = "#{ENV["XDG_RUNTIME_DIR"]}/"
else
# runtime dir has no fallback
return nil
end

# runtime dir must necessarily have certain permissions
unless File.directory?(s) && File.info(s).permissions.value == 0o0700
return nil
else
l = [s]
end

else
raise ArgumentError.new(
"type must be one of: :data, :config, :cache, :runtime"
)
end

# return result with slashes deduplicated and a trailing slash present.
l ? l.map { |s| s.gsub(/\/+/, "/").sub(/[^\/]$/, "\\0/") } : nil

end

# for a given *relative_path*, get a full file path built against an
# appropriate base directory
#
# This method takes a *relative_path*, the *type* of that path, and the
# *action* you intend to perform on it, and returns a full path, constructed
# by concatenating the *relative_path* to the most appropriate base
# directory. If an appropriate directory cannot be found, `nil` is returned.
#
# The *type* argument indicates what sort of file you intend to access at the
# full path (`:config`, `:data` ...), and the *action* argument indicates
# what you intend to to do with it (either `:read` or `:write`). Note that
# `:write` takes precedence over `:read`, so that if you intend to both read
# and write, choose `:write`
#
# ### Example
#
# data = nil
# path = XDGBasedir.full_path "relative/path.dat", :data, :read
#
# if path
# data = File.read path
# end
#
def self.full_path(relative_path, type = :config,
action : Symbol = :read) : String?
unless action == :read || action == :write
raise ArgumentError.new(
"action must be one of: :read, :write"
)
end

case type
when :data, :config, :cache, :runtime
if action == :read
unless l = self.read_dirs(type)
return nil
end

# search the read dirs until relative_path is found
l.each { |d|
if File.exists?(d + relative_path)
return d + relative_path
end
}

# or else just return against the first read dir
l[0] + relative_path
else
d = self.write_dir(type)
d ? d + relative_path : nil
end
else
raise ArgumentError.new(
"type must be one of: :data, :config, :cache, :runtime"
)
end
end

# for a given *relative_path*, get a full file path built against an
# appropriate base directory
#
# This method takes a *relative_path*, the *type* of that path, and the
# access *mode* with which that file is to be opened, and returns a full
# path, constructed by concatenating the *relative_path* to the most
# appropriate base directory. If an appropriate directory cannot be found,
# `nil` is returned.
#
# The *type* indicates what sort of file you intend to access at the full
# path (`:config`, `:data` ...), and the *mode* argument is a string, in the
# common format used by both C's `fopen` and Crystal's `File.open`. For more
# information, refer to `man fopen` or the Crystal standard library
# documentation for [File](https://crystal-lang.org/api/latest/File.html).
#
# This overloaded version of `full_path` is added for convenience, as, if you
# intend to call `File.open` on the produced full path, it might be easier to
# use the same *mode* argument for both methods.
#
# ### Example
#
# data = nil
# mode = "r"
# path = XDGBasedir.full_path "relative/path.dat", :data, mode
#
# if path
# data = File.open(path, mode) do |file|
# file.gets_to_end
# end
# end
#
def self.full_path(relative_path, type = :config,
mode : String = "r") : String?
if /^r[^+]*$/.match(mode)
self.full_path(relative_path, type, :read)
else
self.full_path(relative_path, type, :write)
end
end

end

Loading…
Cancel
Save