Set file permissions explicitly so it works properly on Linux

This commit is contained in:
Sami Samhuri 2019-12-24 23:57:23 -08:00
parent 56833f88e7
commit 2fe6bfc73f
14 changed files with 272 additions and 96 deletions

View file

@ -0,0 +1,18 @@
//
// DirectoryCreating.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-24.
//
import Foundation
protocol DirectoryCreating {
func createDirectory(at url: URL) throws
}
extension FileManager: DirectoryCreating {
func createDirectory(at url: URL) throws {
try createDirectory(at: url, withIntermediateDirectories: true, attributes: [.posixPermissions: FilePermissions.directoryDefault.rawValue])
}
}

View file

@ -0,0 +1,47 @@
//
// FilePermissions.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-24.
//
import Foundation
struct FilePermissions: CustomStringConvertible {
let user: Permissions
let group: Permissions
let other: Permissions
var description: String {
[user, group, other].map { $0.description }.joined()
}
static let `default`: FilePermissions = "rw-r--r--"
static let directoryDefault: FilePermissions = "rwxr-xr-x"
}
extension FilePermissions {
init(string: String) {
user = Permissions(string: String(string.prefix(3)))
group = Permissions(string: String(string.dropFirst(3).prefix(3)))
other = Permissions(string: String(string.dropFirst(6).prefix(3)))
}
}
extension FilePermissions: RawRepresentable {
var rawValue: Int16 {
user.rawValue << 6 | group.rawValue << 3 | other.rawValue
}
init(rawValue: Int16) {
user = Permissions(rawValue: rawValue >> 6 & 7)
group = Permissions(rawValue: rawValue >> 3 & 7)
other = Permissions(rawValue: rawValue >> 0 & 7)
}
}
extension FilePermissions: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(string: value)
}
}

View file

@ -0,0 +1,21 @@
//
// FilePermissionsSetting.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-24.
//
import Foundation
protocol FilePermissionsSetting {
func setPermissions(_ permissions: FilePermissions, ofItemAt fileURL: URL) throws
}
extension FileManager: FilePermissionsSetting {
func setPermissions(_ permissions: FilePermissions, ofItemAt fileURL: URL) throws {
let attributes: [FileAttributeKey: Any] = [
.posixPermissions: permissions.rawValue,
]
try setAttributes(attributes, ofItemAtPath: fileURL.path)
}
}

View file

@ -0,0 +1,35 @@
//
// FileWriter.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-24.
//
import Foundation
/// On Linux umask doesn't seem to be respected and files are written without
/// group and other read permissions by default. This class explicitly sets
/// permissions and then it works properly on macOS and Linux.
final class FileWriter {
typealias FileManager = DirectoryCreating & FilePermissionsSetting
let fileManager: FileManager
init(fileManager: FileManager = Foundation.FileManager.default) {
self.fileManager = fileManager
}
}
extension FileWriter: FileWriting {
func write(data: Data, to fileURL: URL, permissions: FilePermissions) throws {
try fileManager.createDirectory(at: fileURL.deletingLastPathComponent())
try data.write(to: fileURL, options: .atomic)
try fileManager.setPermissions(permissions, ofItemAt: fileURL)
}
func write(string: String, to fileURL: URL, permissions: FilePermissions) throws {
try fileManager.createDirectory(at: fileURL.deletingLastPathComponent())
try string.write(to: fileURL, atomically: true, encoding: .utf8)
try fileManager.setPermissions(permissions, ofItemAt: fileURL)
}
}

View file

@ -0,0 +1,26 @@
//
// FileWriting.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-24.
//
import Foundation
protocol FileWriting {
func write(data: Data, to fileURL: URL) throws
func write(data: Data, to fileURL: URL, permissions: FilePermissions) throws
func write(string: String, to fileURL: URL) throws
func write(string: String, to fileURL: URL, permissions: FilePermissions) throws
}
extension FileWriting {
func write(data: Data, to fileURL: URL) throws {
try write(data: data, to: fileURL, permissions: .default)
}
func write(string: String, to fileURL: URL) throws {
try write(string: string, to: fileURL, permissions: .default)
}
}

View file

@ -0,0 +1,49 @@
//
// Permissions.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-24.
//
import Foundation
struct Permissions: OptionSet {
let rawValue: Int16
static let execute = Permissions(rawValue: 1 << 0)
static let write = Permissions(rawValue: 1 << 1)
static let read = Permissions(rawValue: 1 << 2)
init(rawValue: Int16) {
self.rawValue = rawValue
}
init(string: String) {
self.init(rawValue: 0)
if string[string.startIndex] == "r" {
insert(.read)
}
if string[string.index(string.startIndex, offsetBy: 1)] == "w" {
insert(.write)
}
if string[string.index(string.startIndex, offsetBy: 2)] == "x" {
insert(.execute)
}
}
}
extension Permissions: CustomStringConvertible {
var description: String {
[
contains(.read) ? "r" : "-",
contains(.write) ? "w" : "-",
contains(.execute) ? "x" : "-",
].joined()
}
}
extension Permissions: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(string: value)
}
}

View file

@ -10,11 +10,13 @@ import Ink
final class MarkdownRenderer: Renderer {
let fileManager: FileManager = .default
let fileWriter: FileWriting
let markdownParser = MarkdownParser()
let pageRenderer: MarkdownPageRenderer
init(pageRenderer: MarkdownPageRenderer) {
init(pageRenderer: MarkdownPageRenderer, fileWriter: FileWriting = FileWriter()) {
self.pageRenderer = pageRenderer
self.fileWriter = fileWriter
}
func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
@ -37,8 +39,7 @@ final class MarkdownRenderer: Renderer {
htmlPath = mdFilename.replacingOccurrences(of: ".md", with: "/index.html")
}
let htmlURL = targetDir.appendingPathComponent(htmlPath)
try fileManager.createDirectory(at: htmlURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try pageHTML.write(to: htmlURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: pageHTML, to: htmlURL)
}
func markdownMetadata(from url: URL) throws -> [String: String] {

View file

@ -7,6 +7,54 @@
import Foundation
final class JSONFeedWriter {
let fileWriter: FileWriting
let jsonFeed: JSONFeed
init(jsonFeed: JSONFeed, fileWriter: FileWriting = FileWriter()) {
self.jsonFeed = jsonFeed
self.fileWriter = fileWriter
}
func writeFeed(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
let feed = Feed(
title: site.title,
home_page_url: site.url.absoluteString,
feed_url: site.url.appendingPathComponent(jsonFeed.path).absoluteString,
author: FeedAuthor(
name: site.author,
avatar: jsonFeed.avatarPath.map(site.url.appendingPathComponent)?.absoluteString,
url: site.url.absoluteString
),
icon: jsonFeed.iconPath.map(site.url.appendingPathComponent)?.absoluteString,
favicon: jsonFeed.faviconPath.map(site.url.appendingPathComponent)?.absoluteString,
items: try posts.map { post in
let url = site.url.appendingPathComponent(post.path)
return FeedItem(
title: post.isLink ? "\(post.title)" : post.title,
date_published: post.date,
id: url.absoluteString,
url: url.absoluteString,
external_url: post.link?.absoluteString,
author: FeedAuthor(name: post.author, avatar: nil, url: nil),
content_html: try templateRenderer.renderFeedPost(post, site: site, assets: .none()),
tags: post.tags
)
}
)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
#if os(Linux)
encoder.outputFormatting = [.prettyPrinted]
#else
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
#endif
let feedJSON = try encoder.encode(feed)
let feedURL = targetURL.appendingPathComponent(jsonFeed.path)
try fileWriter.write(data: feedJSON, to: feedURL)
}
}
private struct Feed: Codable {
let version = "https://jsonfeed.org/version/1"
let title: String
@ -34,49 +82,3 @@ private struct FeedItem: Codable {
let content_html: String
let tags: [String]
}
final class JSONFeedWriter {
let fileManager: FileManager
let jsonFeed: JSONFeed
init(fileManager: FileManager = .default, feed: JSONFeed) {
self.fileManager = fileManager
self.jsonFeed = feed
}
func writeFeed(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
let items: [FeedItem] = try posts.map { post in
let url = site.url.appendingPathComponent(post.path)
return FeedItem(
title: post.isLink ? "\(post.title)" : post.title,
date_published: post.date,
id: url.absoluteString,
url: url.absoluteString,
external_url: post.link?.absoluteString,
author: FeedAuthor(name: post.author, avatar: nil, url: nil),
content_html: try templateRenderer.renderFeedPost(post, site: site, assets: .none()),
tags: post.tags
)
}
let avatar = jsonFeed.avatarPath.map(site.url.appendingPathComponent)
let feed: Feed = Feed(
title: site.title,
home_page_url: site.url.absoluteString,
feed_url: site.url.appendingPathComponent(jsonFeed.path).absoluteString,
author: FeedAuthor(name: site.author, avatar: avatar?.absoluteString, url: site.url.absoluteString),
icon: jsonFeed.iconPath.map(site.url.appendingPathComponent)?.absoluteString,
favicon: jsonFeed.faviconPath.map(site.url.appendingPathComponent)?.absoluteString,
items: items
)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
#if os(Linux)
encoder.outputFormatting = [.prettyPrinted]
#else
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
#endif
let feedJSON = try encoder.encode(feed)
let feedURL = targetURL.appendingPathComponent(jsonFeed.path)
try feedJSON.write(to: feedURL, options: [.atomic])
}
}

View file

@ -8,18 +8,18 @@
import Foundation
final class RSSFeedWriter {
let fileManager: FileManager
let feed: RSSFeed
let fileWriter: FileWriting
let rssFeed: RSSFeed
init(fileManager: FileManager = .default, feed: RSSFeed) {
self.fileManager = fileManager
self.feed = feed
init(rssFeed: RSSFeed, fileWriter: FileWriting = FileWriter()) {
self.rssFeed = rssFeed
self.fileWriter = fileWriter
}
func writeFeed(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
let feedURL = site.url.appendingPathComponent(feed.path)
let feedURL = site.url.appendingPathComponent(rssFeed.path)
let feedXML = try templateRenderer.renderRSSFeed(posts: posts, feedURL: feedURL, site: site, assets: .none())
let feedFileURL = targetURL.appendingPathComponent(feed.path)
try feedXML.write(to: feedFileURL, atomically: true, encoding: .utf8)
let feedFileURL = targetURL.appendingPathComponent(rssFeed.path)
try fileWriter.write(string: feedXML, to: feedFileURL)
}
}

View file

@ -1,18 +0,0 @@
//
// XMLEscape.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-12.
//
import Foundation
extension String {
@available(*, deprecated)
func escapedForXML() -> String {
replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")
}
}

View file

@ -8,11 +8,11 @@
import Foundation
final class PostWriter {
let fileManager: FileManager
let fileWriter: FileWriting
let outputPath: String
init(fileManager: FileManager = .default, outputPath: String = "posts") {
self.fileManager = fileManager
init(outputPath: String = "posts", fileWriter: FileWriting = FileWriter()) {
self.fileWriter = fileWriter
self.outputPath = outputPath
}
}
@ -26,8 +26,7 @@ extension PostWriter {
let postURL = targetURL
.appendingPathComponent(outputPath)
.appendingPathComponent(filePath(date: post.date, slug: post.slug))
try fileManager.createDirectory(at: postURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try postHTML.write(to: postURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: postHTML, to: postURL)
}
}
@ -42,8 +41,7 @@ extension PostWriter {
func writeRecentPosts(_ recentPosts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
let recentPostsHTML = try templateRenderer.renderRecentPosts(recentPosts, site: site, assets: .none())
let fileURL = targetURL.appendingPathComponent("index.html")
try fileManager.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try recentPostsHTML.write(to: fileURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: recentPostsHTML, to: fileURL)
}
}
@ -53,8 +51,7 @@ extension PostWriter {
func writeArchive(posts: PostsByYear, for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
let archiveHTML = try templateRenderer.renderArchive(postsByYear: posts, site: site, assets: .none())
let archiveURL = targetURL.appendingPathComponent(outputPath).appendingPathComponent("index.html")
try fileManager.createDirectory(at: archiveURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try archiveHTML.write(to: archiveURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: archiveHTML, to: archiveURL)
}
}
@ -66,8 +63,7 @@ extension PostWriter {
let yearDir = targetURL.appendingPathComponent(yearPosts.path)
let yearHTML = try templateRenderer.renderYearPosts(yearPosts, site: site, assets: .none())
let yearURL = yearDir.appendingPathComponent("index.html")
try fileManager.createDirectory(at: yearDir, withIntermediateDirectories: true, attributes: nil)
try yearHTML.write(to: yearURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: yearHTML, to: yearURL)
}
}
}
@ -81,8 +77,7 @@ extension PostWriter {
let monthDir = targetURL.appendingPathComponent(monthPosts.path)
let monthHTML = try templateRenderer.renderMonthPosts(monthPosts, site: site, assets: .none())
let monthURL = monthDir.appendingPathComponent("index.html")
try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
try monthHTML.write(to: monthURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: monthHTML, to: monthURL)
}
}
}

View file

@ -59,7 +59,7 @@ final class PostsPluginBuilder {
let jsonFeedWriter: JSONFeedWriter?
if let jsonFeed = jsonFeed {
jsonFeedWriter = JSONFeedWriter(feed: jsonFeed)
jsonFeedWriter = JSONFeedWriter(jsonFeed: jsonFeed)
}
else {
jsonFeedWriter = nil
@ -67,7 +67,7 @@ final class PostsPluginBuilder {
let rssFeedWriter: RSSFeedWriter?
if let rssFeed = rssFeed {
rssFeedWriter = RSSFeedWriter(feed: rssFeed)
rssFeedWriter = RSSFeedWriter(rssFeed: rssFeed)
}
else {
rssFeedWriter = nil

View file

@ -13,7 +13,7 @@ struct PartialProject {
}
final class ProjectsPlugin: Plugin {
let fileManager: FileManager = .default
let fileWriter: FileWriting
let outputPath: String
let partialProjects: [PartialProject]
let templateRenderer: ProjectsTemplateRenderer
@ -26,12 +26,14 @@ final class ProjectsPlugin: Plugin {
projects: [PartialProject],
templateRenderer: ProjectsTemplateRenderer,
projectAssets: TemplateAssets,
outputPath: String? = nil
outputPath: String? = nil,
fileWriter: FileWriting = FileWriter()
) {
self.partialProjects = projects
self.templateRenderer = templateRenderer
self.projectAssets = projectAssets
self.outputPath = outputPath ?? "projects"
self.fileWriter = fileWriter
}
// MARK: - Plugin methods
@ -53,16 +55,14 @@ final class ProjectsPlugin: Plugin {
}
let projectsDir = targetURL.appendingPathComponent(outputPath)
try fileManager.createDirectory(at: projectsDir, withIntermediateDirectories: true, attributes: nil)
let projectsURL = projectsDir.appendingPathComponent("index.html")
let projectsHTML = try templateRenderer.renderProjects(projects, site: site, assets: .none())
try projectsHTML.write(to: projectsURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: projectsHTML, to: projectsURL)
for project in projects {
let projectURL = projectsDir.appendingPathComponent("\(project.title)/index.html")
let projectHTML = try templateRenderer.renderProject(project, site: site, assets: projectAssets)
try fileManager.createDirectory(at: projectURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try projectHTML.write(to: projectURL, atomically: true, encoding: .utf8)
try fileWriter.write(string: projectHTML, to: projectURL)
}
}
}