paperclip-dropbox 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "erb"
|
3
|
+
require "pathname"
|
4
|
+
require "active_support/core_ext/hash/keys"
|
5
|
+
|
6
|
+
module Paperclip
|
7
|
+
module Storage
|
8
|
+
module Dropbox
|
9
|
+
class Credentials
|
10
|
+
def initialize(credentials)
|
11
|
+
@credentials = credentials
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch(namespace = nil)
|
15
|
+
parse!(namespace)
|
16
|
+
validate!
|
17
|
+
@credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse!(namespace = nil)
|
21
|
+
@credentials =
|
22
|
+
case @credentials
|
23
|
+
when File
|
24
|
+
YAML.load(ERB.new(File.read(@credentials.path)).result)
|
25
|
+
when String, Pathname
|
26
|
+
YAML.load(ERB.new(File.read(@credentials)).result)
|
27
|
+
when Hash
|
28
|
+
@credentials
|
29
|
+
else
|
30
|
+
raise ArgumentError, ":dropbox_credentials is not a path, file, nor a hash"
|
31
|
+
end
|
32
|
+
|
33
|
+
@credentials.stringify_keys!
|
34
|
+
@credentials = @credentials[namespace.to_s] || @credentials
|
35
|
+
@credentials.symbolize_keys!
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate!
|
39
|
+
validate_presence!
|
40
|
+
validate_inclusion!
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def validate_presence!
|
46
|
+
REQUIRED_KEYS.each do |key|
|
47
|
+
value = @credentials.fetch(key)
|
48
|
+
raise KeyError, ":#{key} credential is nil" if value.nil?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_inclusion!
|
53
|
+
if @credentials[:access_type] and not %w[dropbox app_folder].include?(@credentials[:access_type])
|
54
|
+
raise KeyError, %(:access_type must be either "dropbox" or "app_folder" (was "#{@credentials[:access_type]}"))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
REQUIRED_KEYS = [
|
59
|
+
:app_key, :app_secret,
|
60
|
+
:access_token, :access_token_secret,
|
61
|
+
:user_id
|
62
|
+
]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "active_support/core_ext/object/blank"
|
2
|
+
require "active_support/core_ext/string/strip"
|
3
|
+
|
4
|
+
module Paperclip
|
5
|
+
module Storage
|
6
|
+
module Dropbox
|
7
|
+
class PathGenerator
|
8
|
+
def initialize(attachment, attachment_options)
|
9
|
+
@attachment = attachment
|
10
|
+
@attachment_options = attachment_options
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate(style)
|
14
|
+
path = if normal_path_style?
|
15
|
+
generate_from_string(style)
|
16
|
+
else
|
17
|
+
generate_from_proc(style)
|
18
|
+
end
|
19
|
+
path = File.join("Public", path) if @attachment.full_dropbox?
|
20
|
+
path
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def normal_path_style?
|
26
|
+
@attachment_options[:path].present?
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_from_string(style)
|
30
|
+
@attachment_options[:interpolator].interpolate(@attachment.send(:path_option), @attachment, style)
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_from_proc(style)
|
34
|
+
path = @attachment.instance.instance_exec(style, &file_path_proc)
|
35
|
+
style_suffix = (style != @attachment.default_style ? "_#{style}" : "")
|
36
|
+
|
37
|
+
extension = File.extname(@attachment.original_filename)
|
38
|
+
if extension.present? and path =~ /#{extension}$/
|
39
|
+
path.sub(extension, style_suffix + extension)
|
40
|
+
else
|
41
|
+
path + style_suffix
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Oh my god, this is so ugly, why did I do ever do this? Ah, well, If nothing,
|
46
|
+
# it demonstrates what kind of evil things you can do in Ruby :)
|
47
|
+
def file_path_proc
|
48
|
+
return @attachment_options[:dropbox_options][:path] if @attachment_options[:dropbox_options][:path]
|
49
|
+
|
50
|
+
if @attachment_options[:dropbox_options][:unique_filename]
|
51
|
+
eval %(proc { |style| "\#{self.class.model_name.underscore}_\#{id}_\#{#{@attachment.name}.name}" })
|
52
|
+
else
|
53
|
+
eval %(proc { |style| #{@attachment.name}.original_filename })
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Paperclip
|
4
|
+
module Storage
|
5
|
+
module Dropbox
|
6
|
+
class UrlGenerator
|
7
|
+
def initialize(attachment, attachment_options)
|
8
|
+
@attachment = attachment
|
9
|
+
@attachment_options = attachment_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate(style, options)
|
13
|
+
if @attachment.present?
|
14
|
+
url = @attachment.full_dropbox? ? public_url(style) : private_url(style)
|
15
|
+
url = URI.parse(url)
|
16
|
+
url.query = [url.query, "dl=1"].compact.join("&") if options[:download]
|
17
|
+
url.to_s
|
18
|
+
else
|
19
|
+
@attachment_options[:default_url]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def private_url(style)
|
26
|
+
@attachment.dropbox_client.media(@attachment.path(style))["url"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def public_url(style)
|
30
|
+
url = URI.parse("https://dl.dropboxusercontent.com/u/#{user_id}/")
|
31
|
+
path = @attachment.path(style)
|
32
|
+
path = path.match(/^Public\//).post_match
|
33
|
+
url.merge!(path)
|
34
|
+
url.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def user_id
|
38
|
+
@attachment_options[:dropbox_credentials][:user_id]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,154 +1,80 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require 'erb'
|
1
|
+
require "dropbox_sdk"
|
2
|
+
require "active_support/core_ext/hash/keys"
|
3
|
+
require "paperclip/storage/dropbox/path_generator"
|
4
|
+
require "paperclip/storage/dropbox/url_generator"
|
5
|
+
require "paperclip/storage/dropbox/credentials"
|
7
6
|
|
8
7
|
module Paperclip
|
9
8
|
module Storage
|
10
9
|
module Dropbox
|
11
10
|
def self.extended(base)
|
12
11
|
base.instance_eval do
|
13
|
-
@
|
14
|
-
@
|
15
|
-
|
16
|
-
@
|
17
|
-
|
12
|
+
@options[:dropbox_options] ||= {}
|
13
|
+
@options[:dropbox_credentials] = fetch_credentials
|
14
|
+
@options[:path] = nil if @options[:path] == self.class.default_options[:path]
|
15
|
+
@path_generator = PathGenerator.new(self, @options)
|
16
|
+
@url_generator = UrlGenerator.new(self, @options)
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
21
20
|
def flush_writes
|
22
21
|
@queued_for_write.each do |style, file|
|
23
|
-
|
24
|
-
dropbox_client.put_file(path(style), file.read)
|
25
|
-
else
|
26
|
-
raise FileExists, "file \"#{path(style)}\" already exists on Dropbox"
|
27
|
-
end
|
22
|
+
dropbox_client.put_file(path(style), file.read)
|
28
23
|
end
|
29
24
|
after_flush_writes
|
30
|
-
@queued_for_write
|
25
|
+
@queued_for_write.clear
|
31
26
|
end
|
32
27
|
|
33
28
|
def flush_deletes
|
34
29
|
@queued_for_delete.each do |path|
|
35
30
|
dropbox_client.file_delete(path)
|
36
31
|
end
|
37
|
-
@queued_for_delete
|
32
|
+
@queued_for_delete.clear
|
38
33
|
end
|
39
34
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
false
|
35
|
+
def url(style_or_options = default_style, options = {})
|
36
|
+
options.merge!(style_or_options) if style_or_options.is_a?(Hash)
|
37
|
+
style = style_or_options.is_a?(Hash) ? default_style : style_or_options
|
38
|
+
@url_generator.generate(style, options)
|
45
39
|
end
|
46
40
|
|
47
|
-
def
|
48
|
-
|
49
|
-
style = args.first.is_a?(Symbol) ? args.first : default_style
|
50
|
-
options = args.last.is_a?(Hash) ? args.last : {}
|
51
|
-
query = options[:download] ? "?dl=1" : ""
|
52
|
-
|
53
|
-
if app_folder_mode
|
54
|
-
dropbox_client.media(path(style))['url'] + query
|
55
|
-
else
|
56
|
-
File.join("http://dl.dropbox.com/u/#{user_id}", path_for_url(style) + query)
|
57
|
-
end
|
58
|
-
else
|
59
|
-
@options[:default_url]
|
60
|
-
end
|
41
|
+
def path(style = default_style)
|
42
|
+
@path_generator.generate(style)
|
61
43
|
end
|
62
44
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
else
|
67
|
-
File.join("Public", path_for_url(style))
|
45
|
+
def copy_to_local_file(style = default_style, destination_path)
|
46
|
+
File.open(destination_path, "wb") do |file|
|
47
|
+
file.write(dropbox_client.get_file(path(style)))
|
68
48
|
end
|
69
49
|
end
|
70
50
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
path.sub(original_extension, "#{style_suffix}#{original_extension}")
|
77
|
-
else
|
78
|
-
path + style_suffix + original_extension.to_s
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def dropbox_metadata(style = default_style)
|
83
|
-
dropbox_client.metadata(path(style))
|
84
|
-
end
|
85
|
-
|
86
|
-
def copy_to_local_file(style, destination_path)
|
87
|
-
local_file = File.open(destination_path, "wb")
|
88
|
-
local_file.write(dropbox_client.get_file(path(style)))
|
89
|
-
local_file.close
|
51
|
+
def exists?(style = default_style)
|
52
|
+
metadata = dropbox_client.metadata(path(style))
|
53
|
+
not metadata.nil? and not metadata["is_deleted"]
|
54
|
+
rescue DropboxError
|
55
|
+
false
|
90
56
|
end
|
91
57
|
|
92
58
|
def dropbox_client
|
93
59
|
@dropbox_client ||= begin
|
94
|
-
|
95
|
-
session = DropboxSession.new(
|
96
|
-
session.set_access_token(
|
97
|
-
DropboxClient.new(session,
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def original_extension
|
104
|
-
File.extname(original_filename)
|
105
|
-
end
|
106
|
-
|
107
|
-
def user_id
|
108
|
-
@dropbox_credentials[:user_id]
|
109
|
-
end
|
110
|
-
|
111
|
-
def app_folder_mode
|
112
|
-
@dropbox_credentials[:access_type] == 'app_folder'
|
113
|
-
end
|
114
|
-
|
115
|
-
def file_path
|
116
|
-
return @dropbox_options[:path] if @dropbox_options[:path]
|
117
|
-
|
118
|
-
if @dropbox_options[:unique_filename]
|
119
|
-
eval %(proc { |style| "\#{self.class.model_name.underscore}_\#{id}_\#{#{name}.name}" })
|
120
|
-
else
|
121
|
-
eval %(proc { |style| #{name}.original_filename })
|
60
|
+
credentials = @options[:dropbox_credentials]
|
61
|
+
session = DropboxSession.new(credentials[:app_key], credentials[:app_secret])
|
62
|
+
session.set_access_token(credentials[:access_token], credentials[:access_token_secret])
|
63
|
+
DropboxClient.new(session, credentials[:access_type] || "dropbox")
|
122
64
|
end
|
123
65
|
end
|
124
66
|
|
125
|
-
def
|
126
|
-
|
127
|
-
value = @dropbox_credentials.fetch(key)
|
128
|
-
raise ":#{key} credential is nil" if value.nil?
|
129
|
-
end
|
130
|
-
if @dropbox_credentials[:access_type] and not ['dropbox', 'app_folder'].include?(@dropbox_credentials[:access_type])
|
131
|
-
raise KeyError, ":access_type must be 'dropbox' or 'app_folder'"
|
132
|
-
end
|
133
|
-
end
|
67
|
+
def app_folder?; @options[:dropbox_credentials][:access_type] == "app_folder"; end
|
68
|
+
def full_dropbox?; @options[:dropbox_credentials][:access_type] == "dropbox"; end
|
134
69
|
|
135
|
-
|
136
|
-
result =
|
137
|
-
case credentials
|
138
|
-
when File
|
139
|
-
YAML.load(ERB.new(File.read(credentials.path)).result)
|
140
|
-
when String, Pathname
|
141
|
-
YAML.load(ERB.new(File.read(credentials)).result)
|
142
|
-
when Hash
|
143
|
-
credentials
|
144
|
-
else
|
145
|
-
raise ArgumentError, ":dropbox_credentials are not a path, file, nor a hash"
|
146
|
-
end
|
70
|
+
private
|
147
71
|
|
148
|
-
|
72
|
+
def fetch_credentials
|
73
|
+
environment = defined?(Rails) ? Rails.env : @options[:dropbox_options][:environment]
|
74
|
+
Credentials.new(@options[:dropbox_credentials]).fetch(environment)
|
149
75
|
end
|
150
76
|
|
151
|
-
class FileExists <
|
77
|
+
class FileExists < RuntimeError
|
152
78
|
end
|
153
79
|
end
|
154
80
|
end
|
data/paperclip-dropbox.gemspec
CHANGED
@@ -2,13 +2,12 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
4
|
gem.name = "paperclip-dropbox"
|
5
|
-
gem.version = "1.1.
|
6
|
-
gem.platform = Gem::Platform::RUBY
|
5
|
+
gem.version = "1.1.4"
|
7
6
|
|
8
7
|
gem.homepage = "https://github.com/janko-m/paperclip-dropbox"
|
9
8
|
gem.description = %q{Extends Paperclip with Dropbox storage.}
|
10
9
|
gem.summary = gem.description
|
11
|
-
gem.authors = ["Janko
|
10
|
+
gem.authors = ["Janko Marohnić"]
|
12
11
|
gem.email = ["janko.marohnic@gmail.com"]
|
13
12
|
|
14
13
|
gem.files = Dir["lib/**/*"] + ["README.md", "LICENSE", "paperclip-dropbox.gemspec"]
|
@@ -21,12 +20,11 @@ Gem::Specification.new do |gem|
|
|
21
20
|
gem.add_dependency "paperclip", "~> 3.1"
|
22
21
|
gem.add_dependency "dropbox-sdk", "~> 1.3"
|
23
22
|
|
24
|
-
gem.add_development_dependency "rake"
|
23
|
+
gem.add_development_dependency "rake"
|
25
24
|
gem.add_development_dependency "rspec", "~> 2.11"
|
26
25
|
gem.add_development_dependency "vcr", "~> 2.2"
|
27
|
-
gem.add_development_dependency "
|
28
|
-
gem.add_development_dependency "activerecord", "
|
29
|
-
gem.add_development_dependency "
|
30
|
-
gem.add_development_dependency "
|
31
|
-
gem.add_development_dependency "rest-client", "~> 1.6"
|
26
|
+
gem.add_development_dependency "webmock", ">= 1.8", "< 1.10"
|
27
|
+
gem.add_development_dependency "activerecord", ">= 3.2"
|
28
|
+
gem.add_development_dependency "activerecord-nulldb-adapter", ">= 0.2.3"
|
29
|
+
gem.add_development_dependency "rest-client", ">= 1.6"
|
32
30
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paperclip-dropbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
-
- Janko
|
8
|
+
- Janko Marohnić
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-05-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: paperclip
|
@@ -50,7 +50,7 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - ! '>='
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '0
|
53
|
+
version: '0'
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -58,7 +58,7 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0
|
61
|
+
version: '0'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
63
|
name: rspec
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,27 +92,33 @@ dependencies:
|
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: '2.2'
|
94
94
|
- !ruby/object:Gem::Dependency
|
95
|
-
name:
|
95
|
+
name: webmock
|
96
96
|
requirement: !ruby/object:Gem::Requirement
|
97
97
|
none: false
|
98
98
|
requirements:
|
99
|
-
- -
|
99
|
+
- - ! '>='
|
100
100
|
- !ruby/object:Gem::Version
|
101
|
-
version: '1.
|
101
|
+
version: '1.8'
|
102
|
+
- - <
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '1.10'
|
102
105
|
type: :development
|
103
106
|
prerelease: false
|
104
107
|
version_requirements: !ruby/object:Gem::Requirement
|
105
108
|
none: false
|
106
109
|
requirements:
|
107
|
-
- -
|
110
|
+
- - ! '>='
|
108
111
|
- !ruby/object:Gem::Version
|
109
|
-
version: '1.
|
112
|
+
version: '1.8'
|
113
|
+
- - <
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '1.10'
|
110
116
|
- !ruby/object:Gem::Dependency
|
111
117
|
name: activerecord
|
112
118
|
requirement: !ruby/object:Gem::Requirement
|
113
119
|
none: false
|
114
120
|
requirements:
|
115
|
-
- -
|
121
|
+
- - ! '>='
|
116
122
|
- !ruby/object:Gem::Version
|
117
123
|
version: '3.2'
|
118
124
|
type: :development
|
@@ -120,47 +126,31 @@ dependencies:
|
|
120
126
|
version_requirements: !ruby/object:Gem::Requirement
|
121
127
|
none: false
|
122
128
|
requirements:
|
123
|
-
- -
|
129
|
+
- - ! '>='
|
124
130
|
- !ruby/object:Gem::Version
|
125
131
|
version: '3.2'
|
126
132
|
- !ruby/object:Gem::Dependency
|
127
|
-
name:
|
133
|
+
name: activerecord-nulldb-adapter
|
128
134
|
requirement: !ruby/object:Gem::Requirement
|
129
135
|
none: false
|
130
136
|
requirements:
|
131
|
-
- -
|
132
|
-
- !ruby/object:Gem::Version
|
133
|
-
version: '0.6'
|
134
|
-
type: :development
|
135
|
-
prerelease: false
|
136
|
-
version_requirements: !ruby/object:Gem::Requirement
|
137
|
-
none: false
|
138
|
-
requirements:
|
139
|
-
- - ~>
|
140
|
-
- !ruby/object:Gem::Version
|
141
|
-
version: '0.6'
|
142
|
-
- !ruby/object:Gem::Dependency
|
143
|
-
name: sqlite3
|
144
|
-
requirement: !ruby/object:Gem::Requirement
|
145
|
-
none: false
|
146
|
-
requirements:
|
147
|
-
- - ~>
|
137
|
+
- - ! '>='
|
148
138
|
- !ruby/object:Gem::Version
|
149
|
-
version:
|
139
|
+
version: 0.2.3
|
150
140
|
type: :development
|
151
141
|
prerelease: false
|
152
142
|
version_requirements: !ruby/object:Gem::Requirement
|
153
143
|
none: false
|
154
144
|
requirements:
|
155
|
-
- -
|
145
|
+
- - ! '>='
|
156
146
|
- !ruby/object:Gem::Version
|
157
|
-
version:
|
147
|
+
version: 0.2.3
|
158
148
|
- !ruby/object:Gem::Dependency
|
159
149
|
name: rest-client
|
160
150
|
requirement: !ruby/object:Gem::Requirement
|
161
151
|
none: false
|
162
152
|
requirements:
|
163
|
-
- -
|
153
|
+
- - ! '>='
|
164
154
|
- !ruby/object:Gem::Version
|
165
155
|
version: '1.6'
|
166
156
|
type: :development
|
@@ -168,7 +158,7 @@ dependencies:
|
|
168
158
|
version_requirements: !ruby/object:Gem::Requirement
|
169
159
|
none: false
|
170
160
|
requirements:
|
171
|
-
- -
|
161
|
+
- - ! '>='
|
172
162
|
- !ruby/object:Gem::Version
|
173
163
|
version: '1.6'
|
174
164
|
description: Extends Paperclip with Dropbox storage.
|
@@ -180,6 +170,9 @@ extra_rdoc_files: []
|
|
180
170
|
files:
|
181
171
|
- lib/paperclip-dropbox.rb
|
182
172
|
- lib/paperclip/storage/dropbox.rb
|
173
|
+
- lib/paperclip/storage/dropbox/path_generator.rb
|
174
|
+
- lib/paperclip/storage/dropbox/url_generator.rb
|
175
|
+
- lib/paperclip/storage/dropbox/credentials.rb
|
183
176
|
- lib/paperclip/dropbox.rb
|
184
177
|
- lib/paperclip/dropbox/railtie.rb
|
185
178
|
- lib/paperclip/dropbox/rake.rb
|
@@ -208,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
208
201
|
version: '0'
|
209
202
|
segments:
|
210
203
|
- 0
|
211
|
-
hash:
|
204
|
+
hash: 487816321
|
212
205
|
requirements: []
|
213
206
|
rubyforge_project:
|
214
207
|
rubygems_version: 1.8.23
|