files.com 1.1.743 → 1.1.745

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a422fb27c774606b951cf47d99d36f6480eab1489536c421ec5f9a805a1b477a
4
- data.tar.gz: 32674edcd54f7a5a70e59a50a29cb500029b0400d29af20285ab6dbefd7bd1ba
3
+ metadata.gz: 76561ada788c8c00625ff151cad4800e0e9d8d6daf4acd434baa34b7d28be212
4
+ data.tar.gz: e063b0dcec5148771d97c8cca16cd090c307fff2b55a5c7cfa16942460ad9546
5
5
  SHA512:
6
- metadata.gz: babbaee413ae863433f3f7276342383112da748360da76f7e2bc3c0883aaa82cba1e3987178ee224b5a27f38b32410bc5b0780980c3ab719c97cdfe1435a9fd2
7
- data.tar.gz: '08c007519882b709a66584483fec546a5fb9533fae557d91b180c64f608dc72a9223dc8ad8111f6defccde00959109ce439f09d1f937b9a3d3c77fb6bde0594c'
6
+ metadata.gz: b4cf0e52efc0fe229a2564bd784c579b4220a69cef0a493d6e8acaf20a4084cad8d99ffa36e073ec01e73242939119ee6fc241316fde6c0c706df5cfda9fac2d
7
+ data.tar.gz: 5e6a9e900cb910d8a17218adc1ee61d2a7308b68620e9111bc8f1e424f3a0ba80c26d064617a8d840c63574ba5879444865dca534f318d67d7c339506576cff1
data/README.md CHANGED
@@ -346,16 +346,16 @@ end
346
346
 
347
347
  ## Paths
348
348
 
349
- Working with paths in Files.com involves several important considerations. Understanding how path comparisons are applied helps developers ensure consistency and accuracy across all interactions with the platform.
349
+ Files.com preserves the spelling of file and folder paths while comparing them using shared case and Unicode rules. Use the SDK comparison helpers when matching paths locally.
350
350
  <div></div>
351
351
 
352
352
  ### Capitalization
353
353
 
354
- Files.com compares paths in a **case-insensitive** manner. This means path segments are treated as equivalent regardless of letter casing.
354
+ Files.com uses case-insensitive path matching based on its fixed Unicode comparison map.
355
355
 
356
- For example, all of the following resolve to the same internal path:
356
+ For example, the following paths have the same comparison key:
357
357
 
358
- | Path Variant | Interpreted As |
358
+ | Path Variant | Comparison Key |
359
359
  |---------------------------------------|------------------------------|
360
360
  | `Documents/Reports/Q1.pdf` | `documents/reports/q1.pdf` |
361
361
  | `documents/reports/q1.PDF` | `documents/reports/q1.pdf` |
@@ -370,32 +370,27 @@ See also: [Case Sensitivity Documentation](https://www.files.com/docs/files-and-
370
370
 
371
371
  ### Slashes
372
372
 
373
- All path parameters in Files.com (API, SDKs, CLI, automations, integrations) must **omit leading and trailing slashes**. Paths are always treated as **absolute and slash-delimited**, so only internal `/` separators are used and never at the start or end of the string.
373
+ Use `/` between folder and file names, without leading or trailing slashes. SDK normalization helpers convert backslashes to `/`, remove duplicate separators, and discard exact `.` and `..` components. Discarding `..` leaves the preceding folder name intact.
374
374
 
375
- #### Path Slash Examples
376
- | Path | Valid? | Notes |
377
- |-----------------------------------|--------|-------------------------------|
378
- | `folder/subfolder/file.txt` | ✅ | Correct, internal separators only |
379
- | `/folder/subfolder/file.txt` | ❌ | Leading slash not allowed |
380
- | `folder/subfolder/file.txt/` | ❌ | Trailing slash not allowed |
381
- | `//folder//file.txt` | ❌ | Duplicate separators not allowed |
375
+ | Input | Normalized path |
376
+ |-------|-----------------|
377
+ | `folder/subfolder/file.txt` | `folder/subfolder/file.txt` |
378
+ | `/folder/subfolder/file.txt` | `folder/subfolder/file.txt` |
379
+ | `folder/subfolder/file.txt/` | `folder/subfolder/file.txt` |
380
+ | `//folder//file.txt` | `folder/file.txt` |
381
+ | `folder/../file.txt` | `folder/file.txt` |
382
382
 
383
383
  <div></div>
384
384
 
385
- ### Unicode Normalization
385
+ ### Unicode and Path Comparison
386
386
 
387
- Files.com normalizes all paths using [Unicode NFC (Normalization Form C)](https://www.unicode.org/reports/tr15/#Norm_Forms) before comparison. This ensures consistency across different representations of the same characters.
387
+ Files.com compares paths using a fixed mapping shared by the server and SDKs. It treats case and many accent differences as equivalent: `Résumé.txt` and `resume.txt` identify the same file, as do `q` followed by a combining acute accent and `q`. The mapping also handles other equivalences, such as Hiragana and Katakana. Lowercasing or applying a standard Unicode normalization form alone does not reproduce these rules.
388
388
 
389
- For example, the following two paths are treated as equivalent after NFC normalization:
389
+ SDK comparison helpers normalize path separators and dot segments, then apply the bundled [versioned comparison map](https://github.com/Files-com/files-sdk-javascript/blob/master/shared/path_comparison.json). The [shared examples](https://github.com/Files-com/files-sdk-javascript/blob/master/shared/comparison_examples.json) give exact comparison results for integrations that implement their own matching. The map uses hexadecimal Unicode scalar values as keys: a missing entry preserves the character, an empty replacement removes it, and other replacements may contain several characters. Apply each replacement once without normalizing or lowercasing the result again.
390
390
 
391
- | Input | Normalized Form |
392
- |----------------------------------------|------------------------|
393
- | `uploads/\u0065\u0301.txt` | `uploads/é.txt` |
394
- | `docs/Café/Report.txt` | `docs/Café/Report.txt` |
391
+ Use comparison results only for matching. Send the original path spelling in API requests and preserve it for display and local filenames; comparison results can have a different spelling or length.
395
392
 
396
- - All input must be UTF‑8 encoded.
397
- - Precomposed and decomposed characters are unified.
398
- - This affects search, deduplication, and comparisons across SDKs.
393
+ Trailing whitespace is significant for comparison. `report.txt` and `report.txt ` are different file paths, and SDK helpers preserve spaces, tabs, and newlines. Folder names cannot end in whitespace. See [Unicode Normalization](https://www.files.com/docs/files-and-folders/file-system-semantics/unicode-normalization) for the complete path rules.
399
394
 
400
395
  <div></div>
401
396
 
data/_VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.743
1
+ 1.1.745
@@ -150,9 +150,10 @@ module Files
150
150
  def remote_request(method, url, headers = {}, body = nil)
151
151
  context = RequestLogContext.new
152
152
  context.method = method
153
- context.path = url
153
+ context.path = "[transfer]"
154
+ Util.log_debug("Transfer request", method: method, url: url)
154
155
 
155
- execute_request_with_rescues(Files.base_url, context, skip_body_logging: true) do
156
+ execute_request_with_rescues(Files.base_url, context, skip_body_logging: true, is_transfer: true) do
156
157
  conn.run_request(method, url, body, headers) do |req|
157
158
  req.options.open_timeout = Files.open_timeout
158
159
  req.options.timeout = Files.read_timeout
@@ -215,7 +216,7 @@ module Files
215
216
  raise AuthenticationError, "The provided Session ID is invalid (it contains whitespace)"
216
217
  end
217
218
 
218
- def execute_request_with_rescues(base_url, context, skip_body_logging: false)
219
+ def execute_request_with_rescues(base_url, context, skip_body_logging: false, is_transfer: false)
219
220
  num_retries = 0
220
221
  begin
221
222
  request_start = Time.now
@@ -240,14 +241,18 @@ module Files
240
241
  retry
241
242
  end
242
243
 
244
+ Util.log_debug("Transfer error details", error_message: e.message) if is_transfer
245
+
243
246
  case e
244
247
  when Faraday::ClientError, Faraday::ServerError
245
248
  if e.response and has_error_type?(e)
246
- handle_error_response(e.response, error_context)
249
+ handle_error_response(e.response, error_context, is_transfer: is_transfer)
247
250
  else
248
- handle_network_error(e, error_context, num_retries, base_url)
251
+ handle_network_error(e, error_context, num_retries, base_url, is_transfer: is_transfer)
249
252
  end
250
253
  else
254
+ raise e.exception("Transfer request failed (#{e.class})"), cause: nil if is_transfer
255
+
251
256
  raise
252
257
  end
253
258
  end
@@ -266,7 +271,7 @@ module Files
266
271
  str
267
272
  end
268
273
 
269
- private def handle_error_response(http_resp, context)
274
+ private def handle_error_response(http_resp, context, is_transfer: false)
270
275
  begin
271
276
  resp = Response.from_faraday_hash(http_resp)
272
277
  error_data = resp.data[:error] || resp.data[:errors]
@@ -275,17 +280,22 @@ module Files
275
280
 
276
281
  raise Error, "Unknown error" unless error_data
277
282
  rescue JSON::ParserError, Error
283
+ raise APIError.new("Transfer request failed", http_status: http_resp[:status]), cause: nil if is_transfer
284
+
278
285
  raise general_api_error(http_resp[:status], http_resp[:body])
279
286
  end
280
287
 
281
- error = specific_api_error(resp, error_data, context)
288
+ error = specific_api_error(resp, error_data, context, is_transfer: is_transfer)
282
289
 
283
290
  error.response = resp
284
- raise(error)
291
+ raise error, cause: nil if is_transfer
292
+
293
+ raise error
285
294
  end
286
295
 
287
- private def specific_api_error(resp, error_data, _context)
288
- Util.log_error("API error", status: resp.http_status, error_message: error_data[:message])
296
+ private def specific_api_error(resp, error_data, _context, is_transfer: false)
297
+ message = is_transfer ? "Transfer request failed" : error_data[:message]
298
+ Util.log_error("API error", status: resp.http_status, error_message: message)
289
299
 
290
300
  opts = {
291
301
  http_body: resp.http_body,
@@ -295,28 +305,34 @@ module Files
295
305
  code: error_data[:code] || resp.http_status,
296
306
  }
297
307
 
298
- return APIError.new(error_data[:message], **opts) unless resp&.data&.dig(:type)
308
+ return APIError.new(message, **opts) unless resp&.data&.dig(:type)
299
309
 
300
310
  begin
301
311
  error_type = resp.data[:type].split("/").last
302
312
  error_class_name = "#{error_type.split("-").map(&:capitalize).join}Error"
303
313
  error_class = Files.const_get(error_class_name)
304
- error_class.new(error_data[:message], **opts)
314
+ error_class.new(message, **opts)
305
315
  rescue NameError
306
- APIError.new(error_data[:message], **opts)
316
+ APIError.new(message, **opts)
307
317
  end
308
318
  end
309
319
 
310
- private def handle_network_error(error, _context, num_retries, base_url = nil)
320
+ private def handle_network_error(error, _context, num_retries, base_url = nil, is_transfer: false)
311
321
  base_url ||= Files.base_url
312
322
 
313
- error_message = error.message.empty? ? error.response[:body] : error.message
323
+ error_message = if is_transfer
324
+ error.class.name
325
+ else
326
+ error.message.empty? ? error.response[:body] : error.message
327
+ end
314
328
 
315
329
  Util.log_error("Network error", error_message: error_message)
316
330
  message = "Could not connect to Files.com at URL #{base_url}. Please check your internet connection and try again. If this problem persists, you should check Files.com's service status at https://status.files.com, or contact your primary account representative."
317
331
  message += " Request was retried #{num_retries} times." if num_retries > 0
318
332
  message += "\n\n(Network error: #{error_message})"
319
333
 
334
+ raise APIConnectionError, message, cause: nil if is_transfer
335
+
320
336
  raise APIConnectionError, message
321
337
  end
322
338
 
@@ -372,7 +388,8 @@ module Files
372
388
  end
373
389
 
374
390
  private def log_response_error(context, request_start, error)
375
- Util.log_error("Error", elapsed: Time.now - request_start, error_message: error.message, method: context.method, path: context.path)
391
+ Util.log_error("Error", elapsed: Time.now - request_start, error_type: error.class, method: context.method, path: context.path)
392
+ Util.log_debug("Error details", error_message: error.message)
376
393
  end
377
394
 
378
395
  class RequestLogContext
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Files
4
6
  module PathUtil
5
- TRANSLIT_MAP = "ÀA,ÁA,ÂA,ÃA,ÄA,ÅA,ÆAE,ÇC,ÈE,ÉE,ÊE,ËE,ÌI,ÍI,ÎI,ÏI,ÐD,ÑN,ÒO,ÓO,ÔO,ÕO,ÖO,ØO,ÙU,ÚU,ÛU,ÜU,ÝY,ßss,àa,áa,âa,ãa,äa,åa,æae,çc,èe,ée,êe,ëe,ìi,íi,îi,ïi,ðd,ñn,òo,óo,ôo,õo,öo,øo,ùu,úu,ûu,üu,ýy,ÿy,ĀA,āa,ĂA,ăa,ĄA,ąa,ĆC,ćc,ĈC,ĉc,ĊC,ċc,ČC,čc,ĎD,ďd,ĐD,đd,ĒE,ēe,ĔE,ĕe,ĖE,ėe,ĘE,ęe,ĚE,ěe,ĜG,ĝg,ĞG,ğg,ĠG,ġg,ĢG,ģg,ĤH,ĥh,ĦH,ħh,ĨI,ĩi,ĪI,īi,ĬI,ĭi,ĮI,įi,İI,IJIJ,ijij,ĴJ,ĵj,ĶK,ķk,ĹL,ĺl,ĻL,ļl,ĽL,ľl,ŁL,łl,ŃN,ńn,ŅN,ņn,ŇN,ňn,ʼn'n,ŌO,ōo,ŎO,ŏo,ŐO,őo,ŒOE,œoe,ŔR,ŕr,ŖR,ŗr,ŘR,řr,ŚS,śs,ŜS,ŝs,ŞS,şs,ŠS,šs,ŢT,ţt,ŤT,ťt,ŨU,ũu,ŪU,ūu,ŬU,ŭu,ŮU,ůu,ŰU,űu,ŲU,ųu,ŴW,ŵw,ŶY,ŷy,ŸY,ŹZ,źz,ŻZ,żz,ŽZ,žz".split(",").to_h { |val| [ val[0], val[1..2] ] }.freeze
7
+ COMPARISON_MAP = JSON.parse(File.read(File.expand_path("../../shared/path_comparison.json", __dir__))).fetch("mapping").to_h { |hex, value| [ hex.to_i(16).chr(Encoding::UTF_8), value.freeze ] }.freeze
6
8
 
7
9
  def self.normalize_for_comparison(*args)
8
- unicode_normalize_and_transliterate(normalize(*args).to_s).downcase.rstrip
10
+ normalize(*args).gsub(/[^ -@\[-~]/) { |character| COMPARISON_MAP.fetch(character, character) }
9
11
  end
10
12
 
11
13
  def self.same?(a, b)
@@ -39,14 +41,5 @@ module Files
39
41
  new_string.force_encoding("ISO-8859-1").encode("UTF-8")
40
42
  end
41
43
  end
42
-
43
- private_class_method def self.unicode_normalize_and_transliterate(string)
44
- # convert multi-code-point characters into single-code-point characters
45
- normalized_string = string.unicode_normalize(:nfkc)
46
-
47
- normalized_string.gsub(/[^\x00-\x7f]/u) do |char|
48
- TRANSLIT_MAP[char] || char
49
- end
50
- end
51
44
  end
52
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Files
4
- VERSION = "1.1.743"
4
+ VERSION = "1.1.745"
5
5
  end
@@ -0,0 +1,20 @@
1
+ [
2
+ ["Résumé.TXT", "resume.txt"],
3
+ ["q́/ậ.txt", "q/a.txt"],
4
+ ["ς/Σ/ά", "σ/σ/α"],
5
+ ["か/カ/ガ", "か/か/か"],
6
+ ["private\u00ad\u200d\ufe0f/file", "private/file"],
7
+ ["\u0307", ""],
8
+ ["a\u0001b", "ab"],
9
+ ["folder \t", "folder \t"],
10
+ ["Ა/ა", "Ა/ა"],
11
+ ["\u09cb/\u09c7\u09be", "\u09cb/\u09c7\u09be"],
12
+ ["ка/का/क", "ка/का/क"],
13
+ ["й/и", "й/и"],
14
+ ["日本語", "日本語"],
15
+ ["가/가", "가/가"],
16
+ ["A/B/℀/…", "a/b/a/c/..."],
17
+ ["𐐀/𐐨", "𐐨/𐐨"],
18
+ ["𠀀/𠀁", "𠀀/𠀁"],
19
+ ["ﷺ", "صلى الله"]
20
+ ]
@@ -8,15 +8,15 @@
8
8
  [ "A\\B\\C.TXT", "a/b/c.txt" ],
9
9
  [ "A/B\\C.TXT", "a/b/c.txt" ],
10
10
  [ "//a/b//c.txt", "a/b/c.txt" ],
11
- [ "a/b/c.txt ", "a/b/c.txt" ],
12
- [ "a/b/c.txt\t", "a/b/c.txt" ],
13
- [ "a/b/c.txt\n", "a/b/c.txt" ],
14
- [ "a/b/c.txt\r", "a/b/c.txt" ],
11
+ [ "a/b/c.txt ", "a/b/c.txt "],
12
+ [ "a/b/c.txt\t", "a/b/c.txt\t"],
13
+ [ "a/b/c.txt\n", "a/b/c.txt\n"],
14
+ [ "a/b/c.txt\r", "a/b/c.txt\r"],
15
15
  [ " space_at_beginning", " space_at_beginning"],
16
- [ "space_at_end ", "space_at_end"],
16
+ [ "space_at_end ", "space_at_end "],
17
17
  [ "tab\tseparated", "tab\tseparated"],
18
18
  [ "<title>hello</hello>", "<title>hello</hello>"],
19
- [ "안녕하세요", "안녕하세요" ],
19
+ [ "안녕하세요", "안녕하세요"],
20
20
  [ "こんにちは", "こんにちは" ],
21
21
  [ "今日は", "今日は" ],
22
22
  [ "longest_unicode_character_﷽", "longest_unicode_character_﷽"],
@@ -27,16 +27,16 @@
27
27
  [ "/../../remote\\path//./to/file.txt", "remote/path/to/file.txt" ],
28
28
  [ "one_code_point_ą", "one_code_point_a" ],
29
29
  [ "two_code_points_ą", "two_code_points_a" ],
30
- [ "one_code_point_훯", "one_code_point_훯"],
31
- [ "three_code_points_훯", "three_code_points_훯" ],
30
+ [ "one_code_point_훯", "one_code_point_훯"],
31
+ [ "three_code_points_훯", "three_code_points_훯"],
32
32
  [ "ÞþŊŋŦŧ", "þþŋŋŧŧ" ],
33
33
  [ "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ", "aaaaaaaeceeeeiiiidnoooooouuuuyssaaaaaaaeceeeeiiiidnoooooouuuuyy" ],
34
34
  [ "ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİIJij", "aaaaaaccccccccddddeeeeeeeeeegggggggghhhhiiiiiiiiiijij" ],
35
35
  [ "ĴĵĶķĹĺĻļĽľŁłŃńŅņŇňʼnŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤť", "jjkkllllllllnnnnnnʼnoooooooeoerrrrrrsssssssstttt" ],
36
36
  [ "ŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽž", "uuuuuuuuuuuuwwyyyzzzzzz" ],
37
- [ "😂❤️😍🤣😊🙏💕😭😘👍😅👏😁♥️🔥💔💖💙😢🤔😆🙄💪😉☺️👌🤗", "😂❤️😍🤣😊🙏💕😭😘👍😅👏😁♥️🔥💔💖💙😢🤔😆🙄💪😉☺️👌🤗" ],
38
- [ "💜😔😎😇🌹🤦🎉💞✌️✨🤷😱😌🌸🙌😋💗💚😏💛🙂💓🤩😄😀🖤😃💯🙈👇🎶😒🤭❣️", "💜😔😎😇🌹🤦🎉💞✌️✨🤷😱😌🌸🙌😋💗💚😏💛🙂💓🤩😄😀🖤😃💯🙈👇🎶😒🤭❣️" ],
39
- [ "emoji_‼️", "emoji_!!️" ],
37
+ [ "😂❤️😍🤣😊🙏💕😭😘👍😅👏😁♥️🔥💔💖💙😢🤔😆🙄💪😉☺️👌🤗", "😂❤😍🤣😊🙏💕😭😘👍😅👏😁♥🔥💔💖💙😢🤔😆🙄💪😉☺👌🤗"],
38
+ [ "💜😔😎😇🌹🤦🎉💞✌️✨🤷😱😌🌸🙌😋💗💚😏💛🙂💓🤩😄😀🖤😃💯🙈👇🎶😒🤭❣️", "💜😔😎😇🌹🤦🎉💞✌✨🤷😱😌🌸🙌😋💗💚😏💛🙂💓🤩😄😀🖤😃💯🙈👇🎶😒🤭❣"],
39
+ [ "emoji_‼️", "emoji_!!"],
40
40
  [ "<title>hello<:hello>.txt", "<title>hello<:hello>.txt" ],
41
41
  [ "twodotfile..txt", "twodotfile..txt" ],
42
42
  [ "three/.../dots_path", "three/.../dots_path" ],