aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore9
-rw-r--r--LICENSE21
-rw-r--r--README.md110
-rw-r--r--shard.yml9
-rw-r--r--spec/spec_helper.cr2
-rw-r--r--spec/xdg_basedir_spec.cr50
-rw-r--r--src/xdg_basedir.cr299
7 files changed, 500 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e29dae7
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e32261c
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a72a85b
--- /dev/null
+++ b/README.md
@@ -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).
diff --git a/shard.yml b/shard.yml
new file mode 100644
index 0000000..8146c4c
--- /dev/null
+++ b/shard.yml
@@ -0,0 +1,9 @@
+name: xdg_basedir
+version: 1.0.0
+
+authors:
+ - katherine <shmibs@shmibbles.me>
+
+crystal: 0.27.0
+
+license: MIT
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
new file mode 100644
index 0000000..38f3f4a
--- /dev/null
+++ b/spec/spec_helper.cr
@@ -0,0 +1,2 @@
+require "spec"
+require "../src/xdg_basedir"
diff --git a/spec/xdg_basedir_spec.cr b/spec/xdg_basedir_spec.cr
new file mode 100644
index 0000000..1895439
--- /dev/null
+++ b/spec/xdg_basedir_spec.cr
@@ -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
diff --git a/src/xdg_basedir.cr b/src/xdg_basedir.cr
new file mode 100644
index 0000000..b406841
--- /dev/null
+++ b/src/xdg_basedir.cr
@@ -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