diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..0f438733b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,790 @@ +[*] +charset = utf-8 +end_of_line = crlf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 999 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = 250 +ij_wrap_on_typing = false + +[*.css] +ij_visual_guides = none +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_visual_guides = none +ij_gherkin_keep_indents_on_empty_lines = false + +[*.haml] +indent_size = 2 +ij_visual_guides = none +ij_haml_keep_indents_on_empty_lines = false + +[*.less] +indent_size = 2 +ij_visual_guides = none +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.pp] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = none +ij_puppet_keep_indents_on_empty_lines = false + +[*.properties] +ij_visual_guides = none +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[*.sass] +indent_size = 2 +ij_visual_guides = none +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_visual_guides = none +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.styl] +indent_size = 2 +ij_visual_guides = none +ij_stylus_align_closing_brace_with_properties = false +ij_stylus_blank_lines_around_nested_selector = 1 +ij_stylus_blank_lines_between_blocks = 1 +ij_stylus_brace_placement = 0 +ij_stylus_enforce_quotes_on_format = false +ij_stylus_hex_color_long_format = false +ij_stylus_hex_color_lower_case = false +ij_stylus_hex_color_short_format = false +ij_stylus_hex_color_upper_case = false +ij_stylus_keep_blank_lines_in_code = 2 +ij_stylus_keep_indents_on_empty_lines = false +ij_stylus_keep_single_line_blocks = false +ij_stylus_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_stylus_space_after_colon = true +ij_stylus_space_before_opening_brace = true +ij_stylus_use_double_quotes = true +ij_stylus_value_alignment = 0 + +[.editorconfig] +ij_visual_guides = none +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.qrc,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_visual_guides = none +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.ts}] +ij_continuation_indent_size = 4 +ij_visual_guides = none +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = global +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +ij_visual_guides = none +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_visual_guides = none +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = true +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = true +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = none +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = global +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] +ij_visual_guides = none +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.hcl,*.nomad}] +indent_size = 2 +ij_visual_guides = none +ij_hcl_array_wrapping = 2 +ij_hcl_keep_blank_lines_in_code = 2 +ij_hcl_keep_indents_on_empty_lines = false +ij_hcl_keep_line_breaks = true +ij_hcl_object_wrapping = 2 +ij_hcl_property_alignment = 0 +ij_hcl_property_line_commenter_character = 0 +ij_hcl_space_after_comma = true +ij_hcl_space_before_comma = false +ij_hcl_spaces_around_assignment_operators = true +ij_hcl_spaces_within_braces = false +ij_hcl_spaces_within_brackets = false +ij_hcl_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +ij_visual_guides = none +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = off +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = off +ij_html_uniform_ident = false + +[{*.markdown,*.md}] +ij_visual_guides = none +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 + +[{*.py,*.pyw,hook-crypto}] +ij_visual_guides = none +ij_python_align_collections_and_comprehensions = true +ij_python_align_multiline_imports = true +ij_python_align_multiline_parameters = true +ij_python_align_multiline_parameters_in_calls = true +ij_python_blank_line_at_file_end = true +ij_python_blank_lines_after_imports = 1 +ij_python_blank_lines_after_local_imports = 0 +ij_python_blank_lines_around_class = 1 +ij_python_blank_lines_around_method = 1 +ij_python_blank_lines_around_top_level_classes_functions = 2 +ij_python_blank_lines_before_first_method = 0 +ij_python_call_parameters_new_line_after_left_paren = false +ij_python_call_parameters_right_paren_on_new_line = false +ij_python_call_parameters_wrap = normal +ij_python_dict_alignment = 0 +ij_python_dict_new_line_after_left_brace = false +ij_python_dict_new_line_before_right_brace = false +ij_python_dict_wrapping = 1 +ij_python_from_import_new_line_after_left_parenthesis = false +ij_python_from_import_new_line_before_right_parenthesis = false +ij_python_from_import_parentheses_force_if_multiline = false +ij_python_from_import_trailing_comma_if_multiline = false +ij_python_from_import_wrapping = 1 +ij_python_hang_closing_brackets = false +ij_python_keep_blank_lines_in_code = 1 +ij_python_keep_blank_lines_in_declarations = 1 +ij_python_keep_indents_on_empty_lines = false +ij_python_keep_line_breaks = true +ij_python_method_parameters_new_line_after_left_paren = false +ij_python_method_parameters_right_paren_on_new_line = false +ij_python_method_parameters_wrap = normal +ij_python_new_line_after_colon = false +ij_python_new_line_after_colon_multi_clause = true +ij_python_optimize_imports_always_split_from_imports = false +ij_python_optimize_imports_case_insensitive_order = false +ij_python_optimize_imports_join_from_imports_with_same_source = true +ij_python_optimize_imports_sort_by_type_first = true +ij_python_optimize_imports_sort_imports = true +ij_python_optimize_imports_sort_names_in_from_imports = true +ij_python_space_after_comma = true +ij_python_space_after_number_sign = true +ij_python_space_after_py_colon = true +ij_python_space_before_backslash = true +ij_python_space_before_comma = false +ij_python_space_before_for_semicolon = false +ij_python_space_before_lbracket = false +ij_python_space_before_method_call_parentheses = false +ij_python_space_before_method_parentheses = false +ij_python_space_before_number_sign = true +ij_python_space_before_py_colon = false +ij_python_space_within_empty_method_call_parentheses = false +ij_python_space_within_empty_method_parentheses = false +ij_python_spaces_around_additive_operators = true +ij_python_spaces_around_assignment_operators = true +ij_python_spaces_around_bitwise_operators = true +ij_python_spaces_around_eq_in_keyword_argument = false +ij_python_spaces_around_eq_in_named_parameter = false +ij_python_spaces_around_equality_operators = true +ij_python_spaces_around_multiplicative_operators = true +ij_python_spaces_around_power_operator = true +ij_python_spaces_around_relational_operators = true +ij_python_spaces_around_shift_operators = true +ij_python_spaces_within_braces = false +ij_python_spaces_within_brackets = false +ij_python_spaces_within_method_call_parentheses = false +ij_python_spaces_within_method_parentheses = false +ij_python_use_continuation_indent_for_arguments = false +ij_python_use_continuation_indent_for_collection_and_comprehensions = false +ij_python_use_continuation_indent_for_parameters = true +ij_python_wrap_long_lines = false + +[{*.tf,*.tfvars}] +indent_size = 2 +ij_visual_guides = none +ij_hcl-terraform_array_wrapping = 2 +ij_hcl-terraform_keep_blank_lines_in_code = 2 +ij_hcl-terraform_keep_indents_on_empty_lines = false +ij_hcl-terraform_keep_line_breaks = true +ij_hcl-terraform_object_wrapping = 2 +ij_hcl-terraform_property_alignment = 0 +ij_hcl-terraform_property_line_commenter_character = 0 +ij_hcl-terraform_space_after_comma = true +ij_hcl-terraform_space_before_comma = false +ij_hcl-terraform_spaces_around_assignment_operators = true +ij_hcl-terraform_spaces_within_braces = false +ij_hcl-terraform_spaces_within_brackets = false +ij_hcl-terraform_wrap_long_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_visual_guides = none +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..7b46a68c0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,56 @@ +module.exports = { + root: true, + extends: '@react-native-community', + rules: { + 'prettier/prettier': 0, + "eqeqeq": 2, + "comma-dangle": 0, + curly: 0, + "no-console": 1, + "no-debugger": 1, + "no-extra-semi": 2, + "no-extra-parens": 1, + "no-extra-boolean-cast": 1, + "no-cond-assign": 2, + "no-irregular-whitespace": 2, + "no-undef": 0, + "no-unused-vars": 0, + "semi": 2, + "semi-spacing": 2, + "valid-jsdoc": [ + 1, + { + "requireReturn": false, + "requireParamDescription": false, + "requireReturnDescription": false + } + ], + "radix": 0, + + "react/display-name": 2, + "react/forbid-prop-types": 1, + "react/jsx-boolean-value": 1, + "react/jsx-closing-bracket-location": 1, + "react/jsx-curly-spacing": 1, + "react/jsx-indent-props": 0, + "react/jsx-max-props-per-line": 0, + "react/jsx-no-duplicate-props": 1, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 1, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/jsx-wrap-multilines": 1, + "react/no-danger": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-direct-mutation-state": 1, + "react/no-multi-comp": 1, + "react/no-set-state": 0, + "react/no-unknown-property": 1, + "react/prop-types": 0, + "react/react-in-jsx-scope": 0, + "react/self-closing-comp": 1, + "react/sort-comp": 1, + }, +}; diff --git a/.github/workflows/npm_publish.yml b/.github/workflows/npm_publish.yml new file mode 100644 index 000000000..013398a04 --- /dev/null +++ b/.github/workflows/npm_publish.yml @@ -0,0 +1,36 @@ +# This workflow will publish a package to npm when a release is created. +# https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages + +# TODO: add tests or leave them out, add build steps...? + +name: npm publish + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - run: npm ci + # - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: https://registry.npmjs.org/ + - run: npm ci + # - run: npm run build + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 418c56e5c..8315eeb9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .imdone testconfig -RNFetchBlobTest/ +ReactNativeBlobUtilTest/ test/test-server/public/* !test/test-server/public/github.png @@ -27,6 +27,13 @@ DerivedData *.ipa *.xcuserstate project.xcworkspace +/iOS/pods +Podfile* +contents.xcworkspacedata + +# VSCode +# +.vscode # Android/IJ # @@ -44,3 +51,4 @@ buck-out/ \.buckd/ android/app/libs android/keystores/debug.keystore +/ios/ReactNativeBlobUtil.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..427e52721 --- /dev/null +++ b/.npmignore @@ -0,0 +1,90 @@ +# JS +node_modules +yarn.lock + +# Project files +CONTRIBUTORS.md +CONTRIBUTING.md +CODE_OF_CONDUCT.md + +# Config files +.babelrc +babel.config.js +.editorconfig +.eslintrc +.flowconfig +.watchmanconfig +jsconfig.json +.npmrc +.gitattributes +.circleci +*.coverage.json +.opensource +.circleci +.eslintignore +codecov.yml + +# Example +img/ +example/ +examples/ +app.json + +# Android +android/*/build/ +android/gradlew +android/build +android/gradlew.bat +android/gradle/ +android/com_crashlytics_export_strings.xml +android/local.properties +android/.gradle/ +android/.signing/ +android/.idea/gradle.xml +android/.idea/libraries/ +android/.idea/workspace.xml +android/.idea/tasks.xml +android/.idea/.name +android/.idea/compiler.xml +android/.idea/copyright/profiles_settings.xml +android/.idea/encodings.xml +android/.idea/misc.xml +android/.idea/modules.xml +android/.idea/scopes/scope_settings.xml +android/.idea/vcs.xml +android/*.iml +android/.settings +android/proguard-rules.pro + +# iOS +ios/*.xcodeproj/xcuserdata +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 +*.xcuserstate +project.xcworkspace/ +xcuserdata/ +ios/build/ + +# Misc +.DS_Store +.DS_Store? +*.DS_Store +coverage.android.json +coverage.ios.json +coverage +npm-debug.log +.github +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.dbandroid/gradle +docs +.idea +tests/ +bin/test.js +codorials +.vscode +.nyc_output diff --git a/Migration.md b/Migration.md new file mode 100644 index 000000000..b35b84d26 --- /dev/null +++ b/Migration.md @@ -0,0 +1,41 @@ +# Migration to the New Architecture + +This file is a tracker for what has been done to work on the migration of this library and to keep also track of the various todo: + +## TODO + +- [] Write JS spec in Flow for the New Architecture +- [] Implent the new Native Code on iOS +- [] Implent the new Native Code on Android +- [] Test on OldArch app (iOS) +- [] Test on OldArch app (Android) +- [] Test on NewArch app (iOS) +- [] Test on NewArch app (Android) +- [] Open PR + +## Done + +### Setup +1. Forked and cloned the repo +1. Checked the list of APIs to migrate. +1. Created an `OldArch` app (0.70) configured for the Old Architecture. +1. Run the app to make sure that it works properly. +1. Created a `NewArch` app (0.70) configured for the New Architecture. +1. Switched the flags for iOS and Android to have the new arch enabled by default +1. Run the app to make sure that it works properly. +1. Moved the apps in the `examples` folder + +### Installing blob-utils +1. Move to `OldArch` +1. Run `yarn add ../..` to install the blob utils. +1. `cd ios` +1. `bundle install && bundle exec pod install` +1. `cd ..` +1. `npx react-native run-ios` +1. Copy the app JS code from `examples/ReactNativeBlobUtil/App.js` to `examples/OldArch/App.js` +1. Fixed various JS issues +1. The app depends on the `Picker`. *Note:* It does not support the New Arch, we need to figure out another way to choose. + 1. run `yarn add @react-native-picker/picker` + 1. run `bundle exec pod install` from the iOS folder + 1. re-run the app +1. Repeat the above steps for `NewArch` diff --git a/README.md b/README.md index a016b72b3..f165b3450 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,24 @@ -# rn-fetch-blob -[![release](https://img.shields.io/github/release/joltup/rn-fetch-blob.svg?style=flat-square)](https://github.com/joltup/rn-fetch-blob/releases) [![npm](https://img.shields.io/npm/v/rn-fetch-blob.svg?style=flat-square)](https://www.npmjs.com/package/rn-fetch-blob) ![](https://img.shields.io/badge/PR-Welcome-brightgreen.svg?style=flat-square) [![](https://img.shields.io/badge/Wiki-Public-brightgreen.svg?style=flat-square)](https://github.com/joltup/rn-fetch-blob/wiki) [![npm](https://img.shields.io/npm/l/rn-fetch-blob.svg?maxAge=2592000&style=flat-square)]() +# react-native-blob-util + +[![release](https://img.shields.io/github/release/RonRadtke/react-native-blob-util.svg?style=flat-square)](https://github.com/RonRadtke/react-native-blob-util/releases) [![npm](https://img.shields.io/npm/v/react-native-blob-util.svg?style=flat-square)](https://www.npmjs.com/package/react-native-blob-util) ![](https://img.shields.io/badge/PR-Welcome-brightgreen.svg?style=flat-square) [![](https://img.shields.io/badge/Wiki-Public-brightgreen.svg?style=flat-square)](https://github.com/RonRadtke/react-native-blob-util/wiki) [![npm](https://img.shields.io/npm/l/react-native-blob-util.svg?maxAge=2592000&style=flat-square)]() A project committed to making file access and data transfer easier and more efficient for React Native developers. -# ⚠️ Unmaintained ⚠️ +# I forked this project to continue working on it. + +This project is a fork of https://www.npmjs.com/package/rn-fetch-blob which on the other hand is a fork of https://github.com/wkh237/react-native-fetch-blob. Both the original repository and its first fork are not maintained anymore. + +The project will be continued in this repository. React-Native-Blob-Util is fully compatible with RN-Fetch-Blob and React-Native-Fetch-Blob. If you want to support the project feel free to contact me or create a pull request with your feature. # Version Compatibility Warning -rn-fetch-blob version 0.10.16 is only compatible with react native 0.60 and up. It should have been a major version bump, we apologize for the mistake. If you are not yet upgraded to react native 0.60 or above, you should remain on rn-fetch-blob version 0.10.15 +react-native-blob-util version **0.17.0** and up is only compatible with react native **0.65** and up. + +react-native-blob-util version **0.10.16** and up is only compatible with react native **0.60** and up. ## Features + +- Access and write data to Android Media Store (e.g. Downloads folder on devices > Android 9) - Transfer data directly from/to storage without BASE64 bridging - File API supports regular files, Asset files, and CameraRoll files - Native-to-native file manipulation API, reduce JS bridging performance loss @@ -17,27 +26,39 @@ rn-fetch-blob version 0.10.16 is only compatible with react native 0.60 and up. - Blob, File, XMLHttpRequest polyfills that make browser-based library available in RN (experimental) - JSON stream supported base on [Oboe.js](https://github.com/jimhigson/oboe.js/) @jimhigson -## TOC (visit [Wiki](https://github.com/joltup/rn-fetch-blob/wiki) to get the complete documentation) +## React Native New Architecture +With React Native 0.68.0 the switch to enable the new aritechture was introduced. +Starting with version 0.17.0 this library introduces support for the new architecture as well. Of course the old architecture will still be supported. +Further information about it and how to use it you can find here: https://reactnative.dev/docs/next/the-new-architecture/landing-page + +## Android 10 & 11 + +Android 10 introduced scoped storage for apps. Apps no longer can create own directories directly on the external storage or access files outside the apps own directories. With version 0.14.0 support for the media storage is implemented. For more information please see the chapter about the mediastore API. +[test](###android-media-storage) +For more information see: https://developer.android.com/training/data-storage + +## TOC (visit [Wiki](https://github.com/RonRadtke/react-native-blob-util/wiki) to get the complete documentation) + * [About](#user-content-about) * [Installation](#user-content-installation) * [HTTP Data Transfer](#user-content-http-data-transfer) - * [Regular Request](#user-content-regular-request) - * [Download file](#download-example-fetch-files-that-need-authorization-token) - * [Upload file](#user-content-upload-example--dropbox-files-upload-api) - * [Multipart/form upload](#user-content-multipartform-data-example--post-form-data-with-file-and-data) - * [Upload/Download progress](#user-content-uploaddownload-progress) - * [Cancel HTTP request](#user-content-cancel-request) - * [Android Media Scanner, and Download Manager Support](#user-content-android-media-scanner-and-download-manager-support) - * [Self-Signed SSL Server](#user-content-self-signed-ssl-server) - * [Transfer Encoding](#user-content-transfer-encoding) - * [Drop-in Fetch Replacement](#user-content-drop-in-fetch-replacement) +* [Regular Request](#user-content-regular-request) +* [Download file](#download-example-fetch-files-that-need-authorization-token) +* [Upload file](#user-content-upload-example--dropbox-files-upload-api) +* [Multipart/form upload](#user-content-multipartform-data-example--post-form-data-with-file-and-data) +* [Upload/Download progress](#user-content-uploaddownload-progress) +* [Cancel HTTP request](#user-content-cancel-request) +* [Android Media Scanner, and Download Manager Support](#user-content-android-media-scanner-and-download-manager-support) +* [Self-Signed SSL Server](#user-content-self-signed-ssl-server) +* [Transfer Encoding](#user-content-transfer-encoding) +* [Drop-in Fetch Replacement](#user-content-drop-in-fetch-replacement) * [File System](#user-content-file-system) - * [File access](#user-content-file-access) - * [File stream](#user-content-file-stream) - * [Manage cached files](#user-content-cache-file-management) +* [File access](#user-content-file-access) +* [File stream](#user-content-file-stream) +* [Manage cached files](#user-content-cache-file-management) * [Web API Polyfills](#user-content-web-api-polyfills) * [Performance Tips](#user-content-performance-tips) -* [API References](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API) +* [API References](https://github.com/RonRadtke/react-native-blob-util/wiki/Fetch-API) * [Caveats](#user-content-caveats) * [Development](#user-content-development) @@ -49,39 +70,52 @@ It is committed to making file access and transfer easier and more efficient for In `0.8.0` we introduced experimental Web API polyfills that make it possible to use browser-based libraries in React Native, such as, [FireBase JS SDK](https://github.com/joltup/rn-firebase-storage-upload-sample) - ## Installation Install package from npm ```sh -npm install --save rn-fetch-blob +npm install --save react-native-blob-util ``` Or if using CocoaPods, add the pod to your `Podfile` ``` -pod 'rn-fetch-blob', - :path => '../node_modules/rn-fetch-blob' +pod 'react-native-blob-util', + :path => '../node_modules/react-native-blob-util' ``` After `0.10.3` you can install this package directly from Github ```sh # replace with any one of the branches -npm install --save github:joltup/rn-fetch-blob# +npm install --save github:RonRadtke/react-native-blob-util# +``` + +**iOS** + +When using the package from npm, run `pod install` from the `ios` directory: + +```sh +cd ios; pod install; cd .. ``` +**Okhttp** + +For using the library okhttp3 is required. It's in general included in react-native. The library uses the okhttp version shipped with react-native or used by your app. For very old devices android devices okhttp 3.12 can be used. + **Manually Link Native Modules** -If automatically linking doesn't work for you, see instructions on [manually linking](https://github.com/joltup/rn-fetch-blob/wiki/Manually-Link-Package#index). +If you're using RN 0.60 or higher, manual linking should not be required anymore. + +If automatically linking doesn't work for you, see instructions on [manually linking](https://github.com/RonRadtke/react-native-blob-util/wiki/Manually-Link-Package#index). **Automatically Link Native Modules** For 0.29.2+ projects, simply link native packages via the following command (note: rnpm has been merged into react-native) ``` -react-native link rn-fetch-blob +react-native link react-native-blob-util ``` As for projects < 0.29 you need `rnpm` to link native packages @@ -93,7 +127,7 @@ rnpm link Optionally, use the following command to add Android permissions to `AndroidManifest.xml` automatically ```sh -RNFB_ANDROID_PERMISSIONS=true react-native link rn-fetch-blob +RNFB_ANDROID_PERMISSIONS=true react-native react-native-blob-util ``` pre 0.29 projects @@ -102,7 +136,7 @@ pre 0.29 projects RNFB_ANDROID_PERMISSIONS=true rnpm link ``` -The link script might not take effect if you have non-default project structure, please visit [the wiki](https://github.com/joltup/rn-fetch-blob/wiki/Manually-Link-Package) to link the package manually. +The link script might not take effect if you have non-default project structure, please visit [the wiki](https://github.com/RonRadtke/react-native-blob-util/wiki/Manually-Link-Package) to link the package manually. **Grant Permission to External storage for Android 5.0 or lower** @@ -112,7 +146,7 @@ If you're going to access external storage (say, SD card storage) for `Android 5 ```diff @@ -145,7 +179,7 @@ If you are going to use the `wifiOnly` flag, you need to add this to `AndroidMan **Grant Access Permission for Android 6.0** -Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app. So adding permissions in `AndroidManifest.xml` won't work for Android 6.0+ devices. To grant permissions in runtime, you might use [PermissionAndroid API](https://facebook.github.io/react-native/docs/permissionsandroid.html). +Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app. So adding permissions in `AndroidManifest.xml` won't work for Android 6.0+ devices. To grant permissions in runtime, you might use [PermissionAndroid API](https://facebook.github.io/react-native/docs/permissionsandroid). ## Usage @@ -154,23 +188,22 @@ ES6 The module uses ES6 style export statement, simply use `import` to load the module. ```js -import RNFetchBlob from 'rn-fetch-blob' +import ReactNativeBlobUtil from 'react-native-blob-util' ``` ES5 -If you're using ES5 require statement to load the module, please add `default`. See [here](https://github.com/joltup/rn-fetch-blob/wiki/Trouble-Shooting#rnfetchblobfetch-is-not-a-function) for more detail. +If you're using ES5 require statement to load the module, please add `default`. See [here](https://github.com/RonRadtke/react-native-blob-util/wiki/Trouble-Shooting#ReactNativeBlobUtilfetch-is-not-a-function) for more detail. ``` -var RNFetchBlob = require('rn-fetch-blob').default +var ReactNativeBlobUtil = require('react-native-blob-util').default ``` ## HTTP Data Transfer - ### Regular Request -After `0.8.0` rn-fetch-blob automatically decides how to send the body by checking its type and `Content-Type` in the header. The rule is described in the following diagram +After `0.8.0` react-native-blob-util automatically decides how to send the body by checking its type and `Content-Type` in the header. The rule is described in the following diagram @@ -178,8 +211,8 @@ To sum up: - To send a form data, the `Content-Type` header does not matter. When the body is an `Array` we will set proper content type for you. - To send binary data, you have two choices, use BASE64 encoded string or path points to a file contains the body. - - If the `Content-Type` containing substring`;BASE64` or `application/octet` the given body will be considered as a BASE64 encoded data which will be decoded to binary data as the request body. - - Otherwise, if a string starts with `RNFetchBlob-file://` (which can simply be done by `RNFetchBlob.wrap(PATH_TO_THE_FILE)`), it will try to find the data from the URI string after `RNFetchBlob-file://` and use it as the request body. +- If the `Content-Type` containing substring`;BASE64` or `application/octet` the given body will be considered as a BASE64 encoded data which will be decoded to binary data as the request body. +- Otherwise, if a string starts with `ReactNativeBlobUtil-file://` (which can simply be done by `ReactNativeBlobUtil.wrap(PATH_TO_THE_FILE)`), it will try to find the data from the URI string after `ReactNativeBlobUtil-file://` and use it as the request body. - To send the body as-is, simply use a `Content-Type` header not containing `;BASE64` or `application/octet`. > It is Worth to mentioning that the HTTP request uses cache by default, if you're going to disable it simply add a Cache-Control header `'Cache-Control' : 'no-store'` @@ -193,27 +226,28 @@ Most simple way is download to memory and stored as BASE64 encoded string, this ```js // send http request in a new thread (using native code) -RNFetchBlob.fetch('GET', 'http://www.example.com/images/img1.png', { - Authorization : 'Bearer access-token...', +ReactNativeBlobUtil.fetch('GET', 'http://www.example.com/images/img1.png', { + Authorization: 'Bearer access-token...', // more headers .. - }) - .then((res) => { - let status = res.info().status; - - if(status == 200) { - // the conversion is done in native code - let base64Str = res.base64() - // the following conversions are done in js, it's SYNC - let text = res.text() - let json = res.json() - } else { - // handle other status codes - } - }) - // Something went wrong: - .catch((errorMessage, statusCode) => { - // error handling - }) +}) + .then((res) => { + let status = res.info().status; + + if (status == 200) { + // the conversion is done in native code + let base64Str = res.base64() + // the following conversions are done in js, it's SYNC + let text = res.text() + let json = res.json() + } + else { + // handle other status codes + } + }) + // Something went wrong: + .catch((errorMessage, statusCode) => { + // error handling + }) ``` ### Download to storage directly @@ -223,19 +257,19 @@ If the response data is large, that would be a bad idea to convert it into BASE6 **These files won't be removed automatically, please refer to [Cache File Management](#user-content-cache-file-management)** ```js -RNFetchBlob - .config({ - // add this option that makes response data to be stored as a file, - // this is much more performant. - fileCache : true, - }) - .fetch('GET', 'http://www.example.com/file/example.zip', { - //some headers .. - }) - .then((res) => { - // the temp file path - console.log('The file saved to ', res.path()) - }) +ReactNativeBlobUtil + .config({ + // add this option that makes response data to be stored as a file, + // this is much more performant. + fileCache: true, + }) + .fetch('GET', 'http://www.example.com/file/example.zip', { + //some headers .. + }) + .then((res) => { + // the temp file path + console.log('The file saved to ', res.path()) + }) ``` **Set Temp File Extension** @@ -243,71 +277,91 @@ RNFetchBlob Sometimes you might need a file extension for some reason. For example, when using file path as the source of `Image` component, the path should end with something like .png or .jpg, you can do this by add `appendExt` option to `config`. ```js -RNFetchBlob - .config({ - fileCache : true, - // by adding this option, the temp files will have a file extension - appendExt : 'png' - }) - .fetch('GET', 'http://www.example.com/file/example.zip', { - //some headers .. - }) - .then((res) => { - // the temp file path with file extension `png` - console.log('The file saved to ', res.path()) - // Beware that when using a file path as Image source on Android, - // you must prepend "file://"" before the file path - imageView = - }) +ReactNativeBlobUtil + .config({ + fileCache: true, + // by adding this option, the temp files will have a file extension + appendExt: 'png' + }) + .fetch('GET', 'http://www.example.com/file/example.zip', { + //some headers .. + }) + .then((res) => { + // the temp file path with file extension `png` + console.log('The file saved to ', res.path()) + // Beware that when using a file path as Image source on Android, + // you must prepend "file://"" before the file path + imageView = + }) ``` **Use Specific File Path** -If you prefer a particular file path rather than randomly generated one, you can use `path` option. We've added [several constants](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#dirs) in v0.5.0 which represents commonly used directories. +If you prefer a particular file path rather than randomly generated one, you can use `path` option. We've added [several constants](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#dirs) in v0.5.0 which represents commonly used directories. ```js -let dirs = RNFetchBlob.fs.dirs -RNFetchBlob -.config({ - // response data will be saved to this path if it has access right. - path : dirs.DocumentDir + '/path-to-file.anything' -}) -.fetch('GET', 'http://www.example.com/file/example.zip', { - //some headers .. -}) -.then((res) => { - // the path should be dirs.DocumentDir + 'path-to-file.anything' - console.log('The file saved to ', res.path()) -}) +let dirs = ReactNativeBlobUtil.fs.dirs +ReactNativeBlobUtil + .config({ + // response data will be saved to this path if it has access right. + path: dirs.DocumentDir + '/path-to-file.anything' + }) + .fetch('GET', 'http://www.example.com/file/example.zip', { + //some headers .. + }) + .then((res) => { + // the path should be dirs.DocumentDir + 'path-to-file.anything' + console.log('The file saved to ', res.path()) + }) ``` **These files won't be removed automatically, please refer to [Cache File Management](#user-content-cache-file-management)** -#### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API +**Use File Transformer** + +If you need to perform any processing on the bytes prior to it being written into storage (e.g. if you want it to be encrypted) then you can use `transform` option. NOTE: you will need to set a transformer on the libray (see [Setting a File Transformer](#Setting-A-File-Transformer)) + +```js +ReactNativeBlobUtil + .config({ + // response data will be saved to this path if it has access right. + path: dirs.DocumentDir + '/path-to-file.anything', + transform: true + }) + .fetch('GET', 'http://www.example.com/file/example.zip', { + //some headers .. + }) + .then((res) => { + // the path should be dirs.DocumentDir + 'path-to-file.anything' + console.log('The file saved to ', res.path()) + }) +``` + +#### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API -`rn-fetch-blob` will convert the base64 string in `body` to binary format using native API, this process is done in a separated thread so that it won't block your GUI. +`react-native-blob-util` will convert the base64 string in `body` to binary format using native API, this process is done in a separated thread so that it won't block your GUI. ```js -RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { - Authorization : "Bearer access-token...", +ReactNativeBlobUtil.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { + Authorization: "Bearer access-token...", 'Dropbox-API-Arg': JSON.stringify({ - path : '/img-from-react-native.png', - mode : 'add', - autorename : true, - mute : false + path: '/img-from-react-native.png', + mode: 'add', + autorename: true, + mute: false }), - 'Content-Type' : 'application/octet-stream', + 'Content-Type': 'application/octet-stream', // here's the body you're going to send, should be a BASE64 encoded string // (you can use "base64"(refer to the library 'mathiasbynens/base64') APIs to make one). // The data will be converted to "byte array"(say, blob) before request sent. - }, base64ImageString) - .then((res) => { - console.log(res.text()) - }) - .catch((err) => { - // error handling .. - }) +}, base64ImageString) + .then((res) => { + console.log(res.text()) + }) + .catch((err) => { + // error handling .. + }) ``` ### Upload a file from storage @@ -315,25 +369,25 @@ RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { If you're going to use a `file` as request body, just wrap the path with `wrap` API. ```js -RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { +ReactNativeBlobUtil.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { // dropbox upload headers - Authorization : "Bearer access-token...", + Authorization: "Bearer access-token...", 'Dropbox-API-Arg': JSON.stringify({ - path : '/img-from-react-native.png', - mode : 'add', - autorename : true, - mute : false + path: '/img-from-react-native.png', + mode: 'add', + autorename: true, + mute: false }), - 'Content-Type' : 'application/octet-stream', - // Change BASE64 encoded data to a file path with prefix `RNFetchBlob-file://`. - // Or simply wrap the file path with RNFetchBlob.wrap(). - }, RNFetchBlob.wrap(PATH_TO_THE_FILE)) - .then((res) => { - console.log(res.text()) - }) - .catch((err) => { - // error handling .. - }) + 'Content-Type': 'application/octet-stream', + // Change BASE64 encoded data to a file path with prefix `ReactNativeBlobUtil-file://`. + // Or simply wrap the file path with ReactNativeBlobUtil.wrap(). +}, ReactNativeBlobUtil.wrap(PATH_TO_THE_FILE)) + .then((res) => { + console.log(res.text()) + }) + .catch((err) => { + // error handling .. + }) ``` ### Multipart/form-data example: Post form data with file and data @@ -344,67 +398,71 @@ Elements have property `filename` will be transformed into binary format, otherw ```js - RNFetchBlob.fetch('POST', 'http://www.example.com/upload-form', { - Authorization : "Bearer access-token", - otherHeader : "foo", - 'Content-Type' : 'multipart/form-data', - }, [ +ReactNativeBlobUtil.fetch('POST', 'http://www.example.com/upload-form', { + Authorization: "Bearer access-token", + otherHeader: "foo", + 'Content-Type': 'multipart/form-data', +}, [ // element with property `filename` will be transformed into `file` in form data - { name : 'avatar', filename : 'avatar.png', data: binaryDataInBase64}, + {name: 'avatar', filename: 'avatar.png', data: binaryDataInBase64}, // custom content type - { name : 'avatar-png', filename : 'avatar-png.png', type:'image/png', data: binaryDataInBase64}, + {name: 'avatar-png', filename: 'avatar-png.png', type: 'image/png', data: binaryDataInBase64}, // part file from storage - { name : 'avatar-foo', filename : 'avatar-foo.png', type:'image/foo', data: RNFetchBlob.wrap(path_to_a_file)}, + {name: 'avatar-foo', filename: 'avatar-foo.png', type: 'image/foo', data: ReactNativeBlobUtil.wrap(path_to_a_file)}, // elements without property `filename` will be sent as plain text - { name : 'name', data : 'user'}, - { name : 'info', data : JSON.stringify({ - mail : 'example@example.com', - tel : '12345678' - })}, - ]).then((resp) => { + {name: 'name', data: 'user'}, + { + name: 'info', data: JSON.stringify({ + mail: 'example@example.com', + tel: '12345678' + }) + }, +]).then((resp) => { // ... - }).catch((err) => { +}).catch((err) => { // ... - }) +}) ``` -What if you want to append a file to form data? Just like [upload a file from storage](#user-content-upload-a-file-from-storage) example, wrap `data` by `wrap` API (this feature is only available for `version >= v0.5.0`). On version >= `0.6.2`, it is possible to set custom MIME type when appending a file to form data. But keep in mind when the file is large it's likely to crash your app. Please consider use other strategy (see [#94](https://github.com/joltup/rn-fetch-blob/issues/94)). +What if you want to append a file to form data? Just like [upload a file from storage](#user-content-upload-a-file-from-storage) example, wrap `data` by `wrap` API (this feature is only available for `version >= v0.5.0`). On version >= `0.6.2`, it is possible to set custom MIME type when appending a file to form data. But keep in mind when the file is large it's likely to crash your app. Please consider use other strategy (see [#94](https://github.com/joltup/react-native-blob-util/issues/94)). ```js - RNFetchBlob.fetch('POST', 'http://www.example.com/upload-form', { - Authorization : "Bearer access-token", - otherHeader : "foo", +ReactNativeBlobUtil.fetch('POST', 'http://www.example.com/upload-form', { + Authorization: "Bearer access-token", + otherHeader: "foo", // this is required, otherwise it won't be process as a multipart/form-data request - 'Content-Type' : 'multipart/form-data', - }, [ + 'Content-Type': 'multipart/form-data', +}, [ // append field data from file path { - name : 'avatar', - filename : 'avatar.png', - // Change BASE64 encoded data to a file path with prefix `RNFetchBlob-file://`. - // Or simply wrap the file path with RNFetchBlob.wrap(). - data: RNFetchBlob.wrap(PATH_TO_THE_FILE) + name: 'avatar', + filename: 'avatar.png', + // Change BASE64 encoded data to a file path with prefix `ReactNativeBlobUtil-file://`. + // Or simply wrap the file path with ReactNativeBlobUtil.wrap(). + data: ReactNativeBlobUtil.wrap(PATH_TO_THE_FILE) }, { - name : 'ringtone', - filename : 'ring.mp3', - // use custom MIME type - type : 'application/mp3', - // upload a file from asset is also possible in version >= 0.6.2 - data : RNFetchBlob.wrap(RNFetchBlob.fs.asset('default-ringtone.mp3')) - } + name: 'ringtone', + filename: 'ring.mp3', + // use custom MIME type + type: 'application/mp3', + // upload a file from asset is also possible in version >= 0.6.2 + data: ReactNativeBlobUtil.wrap(ReactNativeBlobUtil.fs.asset('default-ringtone.mp3')) + }, // elements without property `filename` will be sent as plain text - { name : 'name', data : 'user'}, - { name : 'info', data : JSON.stringify({ - mail : 'example@example.com', - tel : '12345678' - })}, - ]).then((resp) => { + {name: 'name', data: 'user'}, + { + name: 'info', data: JSON.stringify({ + mail: 'example@example.com', + tel: '12345678' + }) + }, +]).then((resp) => { // ... - }).catch((err) => { +}).catch((err) => { // ... - }) +}) ``` ### Upload/Download progress @@ -412,48 +470,47 @@ What if you want to append a file to form data? Just like [upload a file from st In `version >= 0.4.2` it is possible to know the upload/download progress. After `0.7.0` IOS and Android upload progress are also supported. ```js - RNFetchBlob.fetch('POST', 'http://www.example.com/upload', { - //... some headers, - 'Content-Type' : 'octet-stream' - }, base64DataString) - // listen to upload progress event - .uploadProgress((written, total) => { - console.log('uploaded', written / total) - }) - // listen to download progress event - .progress((received, total) => { - console.log('progress', received / total) - }) - .then((resp) => { - // ... - }) - .catch((err) => { - // ... - }) + ReactNativeBlobUtil.fetch('POST', 'http://www.example.com/upload', { + //... some headers, + 'Content-Type': 'octet-stream' +}, base64DataString) + // listen to upload progress event + .uploadProgress((written, total) => { + console.log('uploaded', written / total) + }) + // listen to download progress event + .progress((received, total) => { + console.log('progress', received / total) + }) + .then((resp) => { + // ... + }) + .catch((err) => { + // ... + }) ``` -In `0.9.6`, you can specify an object as the first argument which contains `count` and `interval`, to the frequency of progress event (this will be done in the native context a reduce RCT bridge overhead). Notice that `count` argument will not work if the server does not provide response content length. - +In `0.9.6`, you can specify an object as the first argument which contains `count` and `interval`, to the frequency of progress event (this will be done in the native context a reduce RCT bridge overhead). Notice that `count` argument will not work if the server does not provide response content length. ```js - RNFetchBlob.fetch('POST', 'http://www.example.com/upload', { - //... some headers, - 'Content-Type' : 'octet-stream' - }, base64DataString) - // listen to upload progress event, emit every 250ms - .uploadProgress({ interval : 250 },(written, total) => { - console.log('uploaded', written / total) - }) - // listen to download progress event, every 10% - .progress({ count : 10 }, (received, total) => { - console.log('progress', received / total) - }) - .then((resp) => { - // ... - }) - .catch((err) => { - // ... - }) + ReactNativeBlobUtil.fetch('POST', 'http://www.example.com/upload', { + //... some headers, + 'Content-Type': 'octet-stream' +}, base64DataString) + // listen to upload progress event, emit every 250ms + .uploadProgress({interval: 250}, (written, total) => { + console.log('uploaded', written / total) + }) + // listen to download progress event, every 10% + .progress({count: 10}, (received, total) => { + console.log('progress', received / total) + }) + .then((resp) => { + // ... + }) + .catch((err) => { + // ... + }) ``` ### Cancel Request @@ -461,15 +518,17 @@ In `0.9.6`, you can specify an object as the first argument which contains `coun After `0.7.0` it is possible to cancel an HTTP request. Upon cancellation, it throws a promise rejection, be sure to catch it. ```js -let task = RNFetchBlob.fetch('GET', 'http://example.com/file/1') +let task = ReactNativeBlobUtil.fetch('GET', 'http://example.com/file/1') -task.then(() => { ... }) - // handle request cancelled rejection - .catch((err) => { - console.log(err) - }) +task.then(() => { ... +}) + // handle request cancelled rejection + .catch((err) => { + console.log(err) + }) // cancel the request, the callback function is optional -task.cancel((err) => { ... }) +task.cancel((err) => { ... +}) ``` @@ -477,13 +536,13 @@ task.cancel((err) => { ... }) 0.9.0 -If you have existing code that uses `whatwg-fetch`(the official **fetch**), it's not necessary to replace them with `RNFetchblob.fetch`, you can simply use our **Fetch Replacement**. The difference between Official them is official fetch uses [whatwg-fetch](https://github.com/github/fetch) which wraps XMLHttpRequest polyfill under the hood. It's a great library for web developers, but does not play very well with RN. Our implementation is simply a wrapper of our `fetch` and `fs` APIs, so you can access all the features we provided. +If you have existing code that uses `whatwg-fetch`(the official **fetch**), it's not necessary to replace them with `ReactNativeBlobUtil.fetch`, you can simply use our **Fetch Replacement**. The difference between Official them is official fetch uses [whatwg-fetch](https://github.com/github/fetch) which wraps XMLHttpRequest polyfill under the hood. It's a great library for web developers, but does not play very well with RN. Our implementation is simply a wrapper of our `fetch` and `fs` APIs, so you can access all the features we provided. -[See document and examples](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API#fetch-replacement) +[See document and examples](https://github.com/RonRadtke/react-native-blob-util/wiki/Fetch-API#fetch-replacement) ### Android Media Scanner, and Download Manager Support -If you want to make a file in `External Storage` becomes visible in Picture, Downloads, or other built-in apps, you will have to use `Media Scanner` or `Download Manager`. +If you want to make a file in `External Storage` becomes visible in Picture, Downloads, or other built-in apps, you will have to use `Media Scanner` or `Download Manager` or the `Media Storage`. **Media Scanner** @@ -491,19 +550,19 @@ Media scanner scans the file and categorizes by given MIME type, if MIME type no ```js -RNFetchBlob - .config({ - // DCIMDir is in external storage - path : dirs.DCIMDir + '/music.mp3' - }) - .fetch('GET', 'http://example.com/music.mp3') - .then((res) => RNFetchBlob.fs.scanFile([ { path : res.path(), mime : 'audio/mpeg' } ])) - .then(() => { - // scan file success - }) - .catch((err) => { - // scan file error - }) +ReactNativeBlobUtil + .config({ + // DCIMDir is in external storage + path: dirs.DCIMDir + '/music.mp3' + }) + .fetch('GET', 'http://example.com/music.mp3') + .then((res) => ReactNativeBlobUtil.fs.scanFile([{path: res.path(), mime: 'audio/mpeg'}])) + .then(() => { + // scan file success + }) + .catch((err) => { + // scan file error + }) ``` **Download Manager** @@ -517,23 +576,23 @@ When using DownloadManager, `fileCache` and `path` properties in `config` will n When download complete, DownloadManager will generate a file path so that you can deal with it. ```js -RNFetchBlob - .config({ - addAndroidDownloads : { - useDownloadManager : true, // <-- this is the only thing required - // Optional, override notification setting (default to true) - notification : false, - // Optional, but recommended since android DownloadManager will fail when - // the url does not contains a file extension, by default the mime type will be text/plain - mime : 'text/plain', - description : 'File downloaded by download manager.' - } - }) - .fetch('GET', 'http://example.com/file/somefile') - .then((resp) => { - // the path of downloaded file - resp.path() - }) +ReactNativeBlobUtil + .config({ + addAndroidDownloads: { + useDownloadManager: true, // <-- this is the only thing required + // Optional, override notification setting (default to true) + notification: false, + // Optional, but recommended since android DownloadManager will fail when + // the url does not contains a file extension, by default the mime type will be text/plain + mime: 'text/plain', + description: 'File downloaded by download manager.' + } + }) + .fetch('GET', 'http://example.com/file/somefile') + .then((resp) => { + // the path of downloaded file + resp.path() + }) ``` Your app might not have right to remove/change the file created by Download Manager, therefore you might need to [set custom location to the download task](https://github.com/wkh237/react-native-fetch-blob/issues/236). @@ -547,23 +606,23 @@ Your app might not have right to remove/change the file created by Download Mana If you need to display a notification upon the file is downloaded to storage (as the above) or make the downloaded file visible in "Downloads" app. You have to add some options to `config`. ```js -RNFetchBlob.config({ - fileCache : true, - // android only options, these options be a no-op on IOS - addAndroidDownloads : { - // Show notification when response data transmitted - notification : true, - // Title of download notification - title : 'Great ! Download Success ! :O ', - // File description (not notification description) - description : 'An image file.', - mime : 'image/png', - // Make the file scannable by media scanner - mediaScannable : true, - } +ReactNativeBlobUtil.config({ + fileCache: true, + // android only options, these options be a no-op on IOS + addAndroidDownloads: { + // Show notification when response data transmitted + notification: true, + // Title of download notification + title: 'Great ! Download Success ! :O ', + // File description (not notification description) + description: 'An image file.', + mime: 'image/png', + // Make the file scannable by media scanner + mediaScannable: true, + } }) -.fetch('GET', 'http://example.com/image1.png') -.then(...) + .fetch('GET', 'http://example.com/image1.png') + .then(...) ``` **Open Downloaded File with Intent** @@ -572,24 +631,33 @@ This is a new feature added in `0.9.0` if you're going to open a file path using Download and install an APK programmatically -```js +Note: +be sure to specify the path, do not use the default path, because the permission problem causes the installation to fail, parsing the package fails + +default path: `/data/data/com.android.providers.downloads/cache/xxx.apk` // Will cause parsing of the package to fail,Unable to install APK -const android = RNFetchBlob.android +```js -RNFetchBlob.config({ - addAndroidDownloads : { - useDownloadManager : true, - title : 'awesome.apk', - description : 'An APK that will be installed', - mime : 'application/vnd.android.package-archive', - mediaScannable : true, - notification : true, +const android = ReactNativeBlobUtil.android +const dirs = ReactNativeBlobUtil.fs.dirs; +const apkUrl = "http://www.example.com/awesome.apk"; +const fileName = url.substring(url.lastIndexOf('/') + 1); + +ReactNativeBlobUtil.config({ + addAndroidDownloads: { + useDownloadManager: true, + path: `${dirs.DownloadDir}/${fileName}`, // <-- Must specify + title: 'awesome.apk', + description: 'An APK that will be installed', + mime: 'application/vnd.android.package-archive', + mediaScannable: true, + notification: true, } - }) - .fetch('GET', `http://www.example.com/awesome.apk`) - .then((res) => { - android.actionViewIntent(res.path(), 'application/vnd.android.package-archive') - }) +}) + .fetch('GET', `http://www.example.com/awesome.apk`) + .then((res) => { + android.actionViewIntent(res.path(), 'application/vnd.android.package-archive') + }) ``` Or show an image in image viewer @@ -600,35 +668,106 @@ Or show an image in image viewer ## File System +### Android Media Storage + +Android 10 introduced scoped storage and thus new APIs to store files to Documents, Downloads, Music and other collections. Version 0.14.0 introduced an API to access files in the Media Store but also to create and write to new files in the Media Store. In general you only can access files in the Media Store created by your app, or selected by a picker. + +#### CopyToMediaStore + +Copies an existing file from the internal Storage to the Media Store.
An example for downloading a file and storing it to the `downloads` collection + +```js +ReactNativeBlobUtil + .config({ + fileCache: true + }) + .fetch('GET', 'https://example.de/image.png') + .then(async (res) => { + let result = await ReactNativeBlobUtil.MediaCollection.copyToMediaStore({ + name: filename, // name of the file + parentFolder: '', // subdirectory in the Media Store, e.g. HawkIntech/Files to create a folder HawkIntech with a subfolder Files and save the image within this folder + mimeType: 'image/png' // MIME type of the file + }, + 'Download', // Media Collection to store the file in ("Audio" | "Image" | "Video" | "Download") + res.path() // Path to the file being copied in the apps own storage + ); + }); +``` + +This example is taking advantage of the fileCache option to directly store the downloaded file and get a path for.
+Currently it's not possible to write data directly from a string recevied by fetch, but only to copy it from a file. + +#### createMediaFile + +Creates a new file in the specified collection without writing any data + +````js +let path = await ReactNativeBlobUtil.MediaCollection.createMediafile({ + name: filename, // name of the file + parentFolder: '', // subdirectory in the Media Store, e.g. HawkIntech/Files to create a folder HawkIntech with a subfolder Files and save the image within this folder + mimeType: 'image/png' // MIME type of the file + }, 'Download'// Media Collection to store the file in ("Audio" | "Image" | "Video" | "Download") +); +```` + +#### writeMediaFile + +Writes data from a file in the apps storage to an existing entry of the Media Store +````js +await ReactNativeBlobUtil.MediaCollection.writeToMediafile('content://....', // content uri of the entry in the media storage + localpath // path to the file that should be copied +); +```` + +Copies and tranforms data from a file in the apps storage to an existing entry of the Media Store. NOTE: you must set a transformer on the file in order for the transformation to happen (see [Setting a File Transformer](#Setting-A-File-Transformer)). + +````js +await ReactNativeBlobUtil.MediaCollection.writeToMediafileWithTransform('content://....', // content uri of the entry in the media storage + localpath // path to the file that should be copied +); +```` + +#### copyToInternal +Copies an entry form the media storage to the apps internal storage. +````js +let destpath = ReactNativeBlobUtil.dirs.CacheDir + '/image.png'; +await ReactNativeBlobUtil.MediaCollection.copyToInternal('content://....', // content uri of the entry in the media storage + destpath // path to destination the entry should be copied to +); +```` + ### File Access File access APIs were made when developing `v0.5.0`, which helping us write tests, and was not planned to be a part of this module. However, we realized that it's hard to find a great solution to manage cached files, everyone who uses this module may need these APIs for their cases. -Before start using file APIs, we recommend read [Differences between File Source](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#differences-between-file-source) first. +Before start using file APIs, we recommend read [Differences between File Source](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#differences-between-file-source) first. File Access APIs -- [asset (0.6.2)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#assetfilenamestringstring) -- [dirs](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#dirs) -- [createFile](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#createfilepath-data-encodingpromise) -- [writeFile (0.6.0)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise) -- [appendFile (0.6.0) ](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber) -- [readFile (0.6.0)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#readfilepath-encodingpromise) -- [readStream](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream) -- [hash (0.10.9)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#hashpath-algorithm-promise) -- [writeStream](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise) -- [hash](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#hashpath-algorithmpromise) -- [unlink](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#unlinkpathstringpromise) -- [mkdir](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#mkdirpathstringpromise) -- [ls](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#lspathstringpromise) -- [mv](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#mvfromstring-tostringpromise) -- [cp](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#cpsrcstring-deststringpromise) -- [exists](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#existspathstringpromise) -- [isDir](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#isdirpathstringpromise) -- [stat](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#statpathstringpromise) -- [lstat](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#lstatpathstringpromise) -- [scanFile (Android only)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#scanfilepathstringpromise-androi-only) - -See [File API](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API) for more information + +- [asset (0.6.2)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#assetfilenamestringstring) +- [dirs](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#dirs) +- [createFile](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#createfilepath-data-encodingpromise) +- [writeFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise) +- writeFileWithTransform +- [appendFile (0.6.0) ](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber) +- [readFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readfilepath-encodingpromise) +- readFileWithTransform +- [readStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream) +- [hash (0.10.9)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#hashpath-algorithm-promise) +- [writeStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise) +- [hash](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#hashpath-algorithmpromise) +- [unlink](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#unlinkpathstringpromise) +- [mkdir](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#mkdirpathstringpromise) +- [ls](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#lspathstringpromise) +- [mv](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#mvfromstring-tostringpromise) +- [cp](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#cpsrcstring-deststringpromise) +- [exists](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#existspathstringpromise) +- [isDir](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#isdirpathstringpromise) +- [stat](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#statpathstringpromise) +- [lstat](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#lstatpathstringpromise) +- [scanFile (Android only)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#scanfilepathstringpromise-androi-only) + +See [File API](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API) for more information ### File Stream @@ -636,94 +775,92 @@ In `v0.5.0` we've added `writeStream` and `readStream`, which allows your app r When calling `readStream` method, you have to `open` the stream, and start to read data. When the file is large, consider using an appropriate `bufferSize` and `interval` to reduce the native event dispatching overhead (see [Performance Tips](#user-content-performance-tips)) -> The file stream event has a default throttle(10ms) and buffer size which preventing it cause too much overhead to main thread, yo can also [tweak these values](#user-content-performance-tips). +> The file stream event has a default throttle(10ms) and buffer size which preventing it cause too much overhead to main thread, you can also [tweak these values](#user-content-performance-tips). ```js let data = '' -RNFetchBlob.fs.readStream( - // file path - PATH_TO_THE_FILE, - // encoding, should be one of `base64`, `utf8`, `ascii` - 'base64', - // (optional) buffer size, default to 4096 (4095 for BASE64 encoded data) - // when reading file in BASE64 encoding, buffer size must be multiples of 3. - 4095) -.then((ifstream) => { - ifstream.open() - ifstream.onData((chunk) => { - // when encoding is `ascii`, chunk will be an array contains numbers - // otherwise it will be a string - data += chunk - }) - ifstream.onError((err) => { - console.log('oops', err) +ReactNativeBlobUtil.fs.readStream( + // file path + PATH_TO_THE_FILE, + // encoding, should be one of `base64`, `utf8`, `ascii` + 'base64', + // (optional) buffer size, default to 4096 (4095 for BASE64 encoded data) + // when reading file in BASE64 encoding, buffer size must be multiples of 3. + 4095) + .then((ifstream) => { + ifstream.open() + ifstream.onData((chunk) => { + // when encoding is `ascii`, chunk will be an array contains numbers + // otherwise it will be a string + data += chunk + }) + ifstream.onError((err) => { + console.log('oops', err) + }) + ifstream.onEnd(() => { + { - ofstream.write('foo')) -.then(ofstream => ofstream.write('bar')) -.then(ofstream => ofstream.write('foobar')) -.then(ofstream => ofstream.close()) -.catch(console.error) + .then(ofstream => ofstream.write('foo')) + .then(ofstream => ofstream.write('bar')) + .then(ofstream => ofstream.write('foobar')) + .then(ofstream => ofstream.close()) + .catch(console.error) ``` or ```js -RNFetchBlob.fs.writeStream( - PATH_TO_FILE, - // encoding, should be one of `base64`, `utf8`, `ascii` - 'utf8', - // should data append to existing content ? - true +ReactNativeBlobUtil.fs.writeStream( + PATH_TO_FILE, + // encoding, should be one of `base64`, `utf8`, `ascii` + 'utf8', + // should data append to existing content ? + true ) -.then(stream => Promise.all([ - stream.write('foo'), - stream.write('bar'), - stream.write('foobar') -])) -// Use array destructuring to get the stream object from the first item of the array we get from Promise.all() -.then(([stream]) => stream.close()) -.catch(console.error) + .then(stream => Promise.all([ + stream.write('foo'), + stream.write('bar'), + stream.write('foobar') + ])) + // Use array destructuring to get the stream object from the first item of the array we get from Promise.all() + .then(([stream]) => stream.close()) + .catch(console.error) ``` You should **NOT** do something like this: ```js -RNFetchBlob.fs.writeStream( - PATH_TO_FILE, - // encoding, should be one of `base64`, `utf8`, `ascii` - 'utf8', - // should data append to existing content ? - true) -.then((ofstream) => { - // BAD IDEA - Don't do this, those writes are unchecked: - ofstream.write('foo') - ofstream.write('bar') - ofstream.close() -}) -.catch(console.error) // Cannot catch any write() errors! +ReactNativeBlobUtil.fs.writeStream( + PATH_TO_FILE, + // encoding, should be one of `base64`, `utf8`, `ascii` + 'utf8', + // should data append to existing content ? + true) + .then((ofstream) => { + // BAD IDEA - Don't do this, those writes are unchecked: + ofstream.write('foo') + ofstream.write('bar') + ofstream.close() + }) + .catch(console.error) // Cannot catch any write() errors! ``` -The problem with the above code is that the promises from the `ofstream.write()` calls are detached and "Lost". -That means the entire promise chain A) resolves without waiting for the writes to finish and B) any errors caused by them are lost. -That code may _seem_ to work if there are no errors, but those writes are of the type "fire and forget": You start them and then turn away and never know if they really succeeded. +The problem with the above code is that the promises from the `ofstream.write()` calls are detached and "Lost". That means the entire promise chain A) resolves without waiting for the writes to finish and B) any errors caused by them are lost. That code may _seem_ to work if there are no errors, but those writes are of the type "fire and forget": You start them and then turn away and never know if they really succeeded. ### Cache File Management @@ -731,20 +868,20 @@ When using `fileCache` or `path` options along with `fetch` API, response data w ```js - // remove file using RNFetchblobResponse.flush() object method - RNFetchblob.config({ - fileCache : true - }) - .fetch('GET', 'http://example.com/download/file') - .then((res) => { - // remove cached file from storage - res.flush() - }) - - // remove file by specifying a path - RNFetchBlob.fs.unlink('some-file-path').then(() => { +// remove file using ReactNativeBlobUtilResponse.flush() object method +ReactNativeBlobUtil.config({ + fileCache: true +}) + .fetch('GET', 'http://example.com/download/file') + .then((res) => { + // remove cached file from storage + res.flush() + }) + +// remove file by specifying a path +ReactNativeBlobUtil.fs.unlink('some-file-path').then(() => { // ... - }) +}) ``` @@ -752,33 +889,34 @@ You can also group requests by using `session` API and use `dispose` to remove t ```js - RNFetchblob.config({ - fileCache : true - }) - .fetch('GET', 'http://example.com/download/file') - .then((res) => { - // set session of a response - res.session('foo') - }) +ReactNativeBlobUtil.config({ + fileCache: true +}) + .fetch('GET', 'http://example.com/download/file') + .then((res) => { + // set session of a response + res.session('foo') + }) - RNFetchblob.config({ +ReactNativeBlobUtil.config({ // you can also set session beforehand - session : 'foo' - fileCache : true - }) - .fetch('GET', 'http://example.com/download/file') - .then((res) => { - // ... - }) - - // or put an existing file path to the session - RNFetchBlob.session('foo').add('some-file-path') - // remove a file path from the session - RNFetchBlob.session('foo').remove('some-file-path') - // list paths of a session - RNFetchBlob.session('foo').list() - // remove all files in a session - RNFetchBlob.session('foo').dispose().then(() => { ... }) + session: 'foo' + fileCache: true +}) + .fetch('GET', 'http://example.com/download/file') + .then((res) => { + // ... + }) + +// or put an existing file path to the session +ReactNativeBlobUtil.session('foo').add('some-file-path') +// remove a file path from the session +ReactNativeBlobUtil.session('foo').remove('some-file-path') +// list paths of a session +ReactNativeBlobUtil.session('foo').list() +// remove all files in a session +ReactNativeBlobUtil.session('foo').dispose().then(() => { ... +}) ``` @@ -787,53 +925,147 @@ You can also group requests by using `session` API and use `dispose` to remove t After `0.9.4`, the `Chunked` transfer encoding is disabled by default due to some service provider may not support chunked transfer. To enable it, set `Transfer-Encoding` header to `Chunked`. ```js -RNFetchBlob.fetch('POST', 'http://example.com/upload', { 'Transfer-Encoding' : 'Chunked' }, bodyData) +ReactNativeBlobUtil.fetch('POST', 'http://example.com/upload', {'Transfer-Encoding': 'Chunked'}, bodyData) ``` ### Self-Signed SSL Server -By default, rn-fetch-blob does NOT allow connection to unknown certification provider since it's dangerous. To connect a server with self-signed certification, you need to add `trusty` to `config` explicitly. This function is available for version >= `0.5.3` +By default, react-native-blob-util does NOT allow connection to unknown certification provider since it's dangerous. To connect a server with self-signed certification, you need to add `trusty` to `config` explicitly. This function is available for version >= `0.5.3` +In addition since ``0.16.0`` you'll have to define your own trust manager for android. +````java +.... +import com.ReactNativeBlobUtil.ReactNativeBlobUtilUtils; +... + +public class MainApplication extends Application implements ReactApplication { + ... + @Override + public void onCreate() { + ... + ReactNativeBlobUtilUtils.sharedTrustManager = x509TrustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } + }; + ... + } +```` + +#### Kotlin +````kotlin +.... +import com.ReactNativeBlobUtil.ReactNativeBlobUtilUtils; +import javax.net.ssl.X509TrustManager +... + +public class MainApplication extends Application implements ReactApplication { + ... + public void onCreate() { + ... + ReactNativeBlobUtilUtils.sharedTrustManager = object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) {} + + override fun checkServerTrusted(chain: Array, authType: String) {} + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + }; + ... + } +```` ```js -RNFetchBlob.config({ - trusty : true -}) -.fetch('GET', 'https://mysite.com') -.then((resp) => { - // ... +ReactNativeBlobUtil.config({ + trusty: true }) + .fetch('GET', 'https://mysite.com') + .then((resp) => { + // ... + }) ``` ### WiFi only requests -If you wish to only route requests through the Wifi interface, set the below configuration. -Note: On Android, the `ACCESS_NETWORK_STATE` permission must be set, and this flag will only work -on API version 21 (Lollipop, Android 5.0) or above. APIs below 21 will ignore this flag. +If you wish to only route requests through the Wifi interface, set the below configuration. Note: On Android, the `ACCESS_NETWORK_STATE` permission must be set, and this flag will only work on API version 21 (Lollipop, Android 5.0) or above. APIs below 21 will ignore this flag. ```js -RNFetchBlob.config({ - wifiOnly : true -}) -.fetch('GET', 'https://mysite.com') -.then((resp) => { - // ... +ReactNativeBlobUtil.config({ + wifiOnly: true }) + .fetch('GET', 'https://mysite.com') + .then((resp) => { + // ... + }) ``` +### Transform Files + +Sometimes you may need the files to be transformed after reading from storage or before writing into storage (eg encryption/decyrption). In order to perform the transformations, use `readFileWithTransform` and `writeFileWithTransform`. NOTE: you must set a transformer on the file in order for the transformation to happen (see [Setting a File Transformer](#Setting-A-File-Transformer)). + ## Web API Polyfills -After `0.8.0` we've made some [Web API polyfills](https://github.com/joltup/rn-fetch-blob/wiki/Web-API-Polyfills-(experimental)) that makes some browser-based library available in RN. +After `0.8.0` we've made some [Web API polyfills](https://github.com/RonRadtke/react-native-blob-util/wiki/Web-API-Polyfills-(experimental)) that makes some browser-based library available in RN. - Blob - XMLHttpRequest (Use our implementation if you're going to use it with Blob) -Here's a [sample app](https://github.com/joltup/rn-firebase-storage-upload-sample) that uses polyfills to upload files to FireBase. + +## Setting A File Transformer + +Setting a file transformer will allow you to specify how data should be transformed whenever the library is writing into storage or reading from storage. A use case for this is if you want the files handled by this library to be encrypted. + +If you want to use a file transformer, you must implement an interface defined in: + +[ReactNativeBlobUtilFileTransformer.h (iOS)](/ios/ReactNativeBlobUtilFileTransformer.h) + +[ReactNativeBlobUtilFileTransformer.java (Android)](/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFileTransformer.java) + +Then you set the File Transformer during app startup + +Android: +```java +public class MainApplication extends Application implements ReactApplication { + ... + @Override + public void onCreate() { + ... + ReactNativeBlobUtilFileTransformer.sharedFileTransformer = new MyCustomEncryptor(); + ... + } +``` + +iOS: +```m +@implementation AppDelegate +... +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + ... + [ReactNativeBlobUtilFileTransformer setFileTransformer: MyCustomEncryptor.new]; + ... +} +``` + +Here are the places where the transformer would apply +- Reading a file from the file system +- Writing a file into the file system +- Http response is downloaded to storage directly ## Performance Tips **Read Stream and Progress Event Overhead** -If the process seems to block JS thread when file is large when reading data via `fs.readStream`. It might because the default buffer size is quite small (4kb) which result in a lot of events triggered from JS thread. Try to increase the buffer size (for example 100kb = 102400) and set a larger interval (available for 0.9.4+, the default value is 10ms) to limit the frequency. +If the process seems to block JS thread when file is large when reading data via `fs.readStream`. It might because the default buffer size is quite small (4kb) which result in a lot of events triggered from JS thread. Try to increase the buffer size (for example 100kb = 102400) and set a larger interval (available for 0.9.4+, the default value is 10ms) to limit the frequency. **Reduce RCT Bridge and BASE64 Overhead** @@ -856,18 +1088,17 @@ If you're going to concatenate files, you don't have to read the data to JS cont ## Caveats * This library does not urlencode unicode characters in URL automatically, see [#146](https://github.com/wkh237/react-native-fetch-blob/issues/146). -* When you create a `Blob` , from an existing file, the file **WILL BE REMOVED** if you `close` the blob. +* When you create a `Blob` , from an existing file, the file **WILL BE REMOVED** if you `close` the blob. * If you replaced `window.XMLHttpRequest` for some reason (e.g. make Firebase SDK work), it will also affect how official `fetch` works (basically it should work just fine). -* When file stream and upload/download progress event slow down your app, consider an upgrade to `0.9.6+`, use [additional arguments](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API#fetchprogressconfig-eventlistenerpromisernfetchblobresponse) to limit its frequency. +* When file stream and upload/download progress event slow down your app, consider an upgrade to `0.9.6+`, use [additional arguments](https://github.com/RonRadtke/react-native-blob-util/wiki/Fetch-API#fetchprogressconfig-eventlistenerpromiseReactNativeBlobUtilresponse) to limit its frequency. * When passing a file path to the library, remove `file://` prefix. -when you got a problem, have a look at [Trouble Shooting](https://github.com/joltup/rn-fetch-blob/wiki/Trouble-Shooting) or [issues labeled Trouble Shooting](https://github.com/joltup/rn-fetch-blob/issues?utf8=✓&q=label:%22trouble%20shooting%22%20), there'd be some helpful information. +when you got a problem, have a look at [Trouble Shooting](https://github.com/RonRadtke/react-native-blob-util/wiki/Trouble-Shooting). ## Changes -See [release notes](https://github.com/joltup/rn-fetch-blob/releases) +See [release notes](https://github.com/RonRadtke/react-native-blob-util/releases) ### Development -If you're interested in hacking this module, check our [development guide](https://github.com/joltup/rn-fetch-blob/wiki/Home), there might be some helpful information. -Please feel free to make a PR or file an issue. +If you're interested in hacking this module, check our [development guide](https://github.com/RonRadtke/react-native-blob-util/wiki/Home), there might be some helpful information. Please feel free to make a PR or file an issue. diff --git a/android.js b/android.js index 3e4faa472..c6094ca0d 100644 --- a/android.js +++ b/android.js @@ -2,54 +2,50 @@ // Use of this source code is governed by a MIT-style license that can be // found in the LICENSE file. -import { - NativeModules, - DeviceEventEmitter, - Platform, - NativeAppEventEmitter, -} from 'react-native' - -const RNFetchBlob:RNFetchBlobNative = NativeModules.RNFetchBlob +import { Platform } from 'react-native'; +import ReactNativeBlobUtil from './codegenSpecs/NativeBlobUtils'; /** * Send an intent to open the file. * @param {string} path Path of the file to be open. * @param {string} mime MIME type string + * @param {string} chooserTitle for chooser, if not set the chooser won't be displayed (see https://developer.android.com/reference/android/content/Intent.html#createChooser(android.content.Intent,%20java.lang.CharSequence)) * @return {Promise} */ -function actionViewIntent(path:string, mime:string = 'text/plain') { +function actionViewIntent(path, mime, chooserTitle) { + if(typeof chooserTitle === 'undefined') chooserTitle = null; if(Platform.OS === 'android') - return RNFetchBlob.actionViewIntent(path, mime) + return ReactNativeBlobUtil.actionViewIntent(path, mime, chooserTitle); else - return Promise.reject('RNFetchBlob.android.actionViewIntent only supports Android.') + return Promise.reject('ReactNativeBlobUtil.android.actionViewIntent only supports Android.'); } -function getContentIntent(mime:string) { +function getContentIntent(mime) { if(Platform.OS === 'android') - return RNFetchBlob.getContentIntent(mime) + return ReactNativeBlobUtil.getContentIntent(mime); else - return Promise.reject('RNFetchBlob.android.getContentIntent only supports Android.') + return Promise.reject('ReactNativeBlobUtil.android.getContentIntent only supports Android.'); } function addCompleteDownload(config) { if(Platform.OS === 'android') - return RNFetchBlob.addCompleteDownload(config) + return ReactNativeBlobUtil.addCompleteDownload(config); else - return Promise.reject('RNFetchBlob.android.addCompleteDownload only supports Android.') + return Promise.reject('ReactNativeBlobUtil.android.addCompleteDownload only supports Android.'); } function getSDCardDir() { if(Platform.OS === 'android') - return RNFetchBlob.getSDCardDir() + return ReactNativeBlobUtil.getSDCardDir(); else - return Promise.reject('RNFetchBlob.android.getSDCardDir only supports Android.') + return Promise.reject('ReactNativeBlobUtil.android.getSDCardDir only supports Android.'); } function getSDCardApplicationDir() { if(Platform.OS === 'android') - return RNFetchBlob.getSDCardApplicationDir() + return ReactNativeBlobUtil.getSDCardApplicationDir(); else - return Promise.reject('RNFetchBlob.android.getSDCardApplicationDir only supports Android.') + return Promise.reject('ReactNativeBlobUtil.android.getSDCardApplicationDir only supports Android.'); } @@ -59,4 +55,4 @@ export default { addCompleteDownload, getSDCardDir, getSDCardApplicationDir, -} +}; diff --git a/android/.editorconfig b/android/.editorconfig new file mode 100644 index 000000000..3bad6ff7e --- /dev/null +++ b/android/.editorconfig @@ -0,0 +1,918 @@ +[*] +charset = utf-8 +end_of_line = crlf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 999 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = 250 +ij_wrap_on_typing = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 99 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = android.**,|,androidx.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|,$*,| +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 99 +ij_java_new_line_after_lparen_in_record_header = false +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_record_header = false +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = false +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = false +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = true +ij_xml_text_wrap = normal +ij_xml_use_custom_settings = true + +[{*.bash,*.sh,*.zsh}] +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[{*.c,*.c++,*.cc,*.cp,*.cpp,*.cu,*.cuh,*.cxx,*.h,*.h++,*.hh,*.hp,*.hpp,*.hxx,*.i,*.icc,*.ii,*.inl,*.ino,*.ipp,*.m,*.mm,*.pch,*.tcc,*.tpp}] +ij_c_add_brief_tag = false +ij_c_add_getter_prefix = true +ij_c_add_setter_prefix = true +ij_c_align_dictionary_pair_values = false +ij_c_align_group_field_declarations = false +ij_c_align_init_list_in_columns = true +ij_c_align_multiline_array_initializer_expression = true +ij_c_align_multiline_assignment = true +ij_c_align_multiline_binary_operation = true +ij_c_align_multiline_chained_methods = false +ij_c_align_multiline_for = true +ij_c_align_multiline_ternary_operation = true +ij_c_array_initializer_comma_on_next_line = false +ij_c_array_initializer_new_line_after_left_brace = false +ij_c_array_initializer_right_brace_on_new_line = false +ij_c_array_initializer_wrap = normal +ij_c_assignment_wrap = off +ij_c_binary_operation_sign_on_next_line = false +ij_c_binary_operation_wrap = normal +ij_c_blank_lines_after_class_header = 0 +ij_c_blank_lines_after_imports = 1 +ij_c_blank_lines_around_class = 1 +ij_c_blank_lines_around_field = 0 +ij_c_blank_lines_around_field_in_interface = 0 +ij_c_blank_lines_around_method = 1 +ij_c_blank_lines_around_method_in_interface = 1 +ij_c_blank_lines_around_namespace = 0 +ij_c_blank_lines_around_properties_in_declaration = 0 +ij_c_blank_lines_around_properties_in_interface = 0 +ij_c_blank_lines_before_imports = 1 +ij_c_blank_lines_before_method_body = 0 +ij_c_block_brace_placement = end_of_line +ij_c_block_brace_style = end_of_line +ij_c_block_comment_at_first_column = true +ij_c_catch_on_new_line = false +ij_c_class_brace_style = end_of_line +ij_c_class_constructor_init_list_align_multiline = true +ij_c_class_constructor_init_list_comma_on_next_line = false +ij_c_class_constructor_init_list_new_line_after_colon = never +ij_c_class_constructor_init_list_new_line_before_colon = if_long +ij_c_class_constructor_init_list_wrap = normal +ij_c_copy_is_deep = false +ij_c_create_interface_for_categories = true +ij_c_declare_generated_methods = true +ij_c_description_include_member_names = true +ij_c_discharged_short_ternary_operator = false +ij_c_do_not_add_breaks = false +ij_c_do_while_brace_force = never +ij_c_else_on_new_line = false +ij_c_enum_constants_comma_on_next_line = false +ij_c_enum_constants_wrap = on_every_item +ij_c_for_brace_force = never +ij_c_for_statement_new_line_after_left_paren = false +ij_c_for_statement_right_paren_on_new_line = false +ij_c_for_statement_wrap = off +ij_c_function_brace_placement = end_of_line +ij_c_function_call_arguments_align_multiline = true +ij_c_function_call_arguments_align_multiline_pars = false +ij_c_function_call_arguments_comma_on_next_line = false +ij_c_function_call_arguments_new_line_after_lpar = false +ij_c_function_call_arguments_new_line_before_rpar = false +ij_c_function_call_arguments_wrap = normal +ij_c_function_non_top_after_return_type_wrap = normal +ij_c_function_parameters_align_multiline = true +ij_c_function_parameters_align_multiline_pars = false +ij_c_function_parameters_comma_on_next_line = false +ij_c_function_parameters_new_line_after_lpar = false +ij_c_function_parameters_new_line_before_rpar = false +ij_c_function_parameters_wrap = normal +ij_c_function_top_after_return_type_wrap = normal +ij_c_generate_additional_eq_operators = true +ij_c_generate_additional_rel_operators = true +ij_c_generate_class_constructor = true +ij_c_generate_comparison_operators_use_std_tie = false +ij_c_generate_instance_variables_for_properties = ask +ij_c_generate_operators_as_members = true +ij_c_header_guard_style_pattern = ${PROJECT_NAME}_${FILE_NAME}_${EXT} +ij_c_if_brace_force = never +ij_c_in_line_short_ternary_operator = true +ij_c_indent_block_comment = true +ij_c_indent_c_struct_members = 4 +ij_c_indent_case_from_switch = true +ij_c_indent_class_members = 4 +ij_c_indent_directive_as_code = false +ij_c_indent_implementation_members = 0 +ij_c_indent_inside_code_block = 4 +ij_c_indent_interface_members = 0 +ij_c_indent_interface_members_except_ivars_block = false +ij_c_indent_namespace_members = 4 +ij_c_indent_preprocessor_directive = 0 +ij_c_indent_visibility_keywords = 0 +ij_c_insert_override = true +ij_c_insert_virtual_with_override = false +ij_c_introduce_auto_vars = false +ij_c_introduce_const_params = false +ij_c_introduce_const_vars = false +ij_c_introduce_generate_property = false +ij_c_introduce_generate_synthesize = true +ij_c_introduce_globals_to_header = true +ij_c_introduce_prop_to_private_category = false +ij_c_introduce_static_consts = true +ij_c_introduce_use_ns_types = false +ij_c_ivars_prefix = _ +ij_c_keep_blank_lines_before_end = 2 +ij_c_keep_blank_lines_before_right_brace = 2 +ij_c_keep_blank_lines_in_code = 2 +ij_c_keep_blank_lines_in_declarations = 2 +ij_c_keep_case_expressions_in_one_line = false +ij_c_keep_control_statement_in_one_line = true +ij_c_keep_directive_at_first_column = true +ij_c_keep_first_column_comment = true +ij_c_keep_line_breaks = true +ij_c_keep_nested_namespaces_in_one_line = false +ij_c_keep_simple_blocks_in_one_line = true +ij_c_keep_simple_methods_in_one_line = true +ij_c_keep_structures_in_one_line = false +ij_c_lambda_capture_list_align_multiline = false +ij_c_lambda_capture_list_align_multiline_bracket = false +ij_c_lambda_capture_list_comma_on_next_line = false +ij_c_lambda_capture_list_new_line_after_lbracket = false +ij_c_lambda_capture_list_new_line_before_rbracket = false +ij_c_lambda_capture_list_wrap = off +ij_c_line_comment_add_space = false +ij_c_line_comment_at_first_column = true +ij_c_method_brace_placement = end_of_line +ij_c_method_call_arguments_align_by_colons = true +ij_c_method_call_arguments_align_multiline = false +ij_c_method_call_arguments_special_dictionary_pairs_treatment = true +ij_c_method_call_arguments_wrap = off +ij_c_method_call_chain_wrap = off +ij_c_method_parameters_align_by_colons = true +ij_c_method_parameters_align_multiline = false +ij_c_method_parameters_wrap = off +ij_c_namespace_brace_placement = end_of_line +ij_c_parentheses_expression_new_line_after_left_paren = false +ij_c_parentheses_expression_right_paren_on_new_line = false +ij_c_place_assignment_sign_on_next_line = false +ij_c_property_nonatomic = true +ij_c_put_ivars_to_implementation = true +ij_c_refactor_compatibility_aliases_and_classes = true +ij_c_refactor_properties_and_ivars = true +ij_c_release_style = ivar +ij_c_retain_object_parameters_in_constructor = true +ij_c_semicolon_after_method_signature = false +ij_c_shift_operation_align_multiline = true +ij_c_shift_operation_wrap = normal +ij_c_show_non_virtual_functions = false +ij_c_space_after_colon = true +ij_c_space_after_colon_in_selector = false +ij_c_space_after_comma = true +ij_c_space_after_cup_in_blocks = false +ij_c_space_after_dictionary_literal_colon = true +ij_c_space_after_for_semicolon = true +ij_c_space_after_init_list_colon = true +ij_c_space_after_method_parameter_type_parentheses = false +ij_c_space_after_method_return_type_parentheses = false +ij_c_space_after_pointer_in_declaration = false +ij_c_space_after_quest = true +ij_c_space_after_reference_in_declaration = false +ij_c_space_after_reference_in_rvalue = false +ij_c_space_after_structures_rbrace = true +ij_c_space_after_superclass_colon = true +ij_c_space_after_type_cast = true +ij_c_space_after_visibility_sign_in_method_declaration = true +ij_c_space_before_autorelease_pool_lbrace = true +ij_c_space_before_catch_keyword = true +ij_c_space_before_catch_left_brace = true +ij_c_space_before_catch_parentheses = true +ij_c_space_before_category_parentheses = true +ij_c_space_before_chained_send_message = true +ij_c_space_before_class_left_brace = true +ij_c_space_before_colon = true +ij_c_space_before_comma = false +ij_c_space_before_dictionary_literal_colon = false +ij_c_space_before_do_left_brace = true +ij_c_space_before_else_keyword = true +ij_c_space_before_else_left_brace = true +ij_c_space_before_for_left_brace = true +ij_c_space_before_for_parentheses = true +ij_c_space_before_for_semicolon = false +ij_c_space_before_if_left_brace = true +ij_c_space_before_if_parentheses = true +ij_c_space_before_init_list = false +ij_c_space_before_init_list_colon = true +ij_c_space_before_method_call_parentheses = false +ij_c_space_before_method_left_brace = true +ij_c_space_before_method_parentheses = false +ij_c_space_before_namespace_lbrace = true +ij_c_space_before_pointer_in_declaration = true +ij_c_space_before_property_attributes_parentheses = false +ij_c_space_before_protocols_brackets = true +ij_c_space_before_quest = true +ij_c_space_before_reference_in_declaration = true +ij_c_space_before_superclass_colon = true +ij_c_space_before_switch_left_brace = true +ij_c_space_before_switch_parentheses = true +ij_c_space_before_template_call_lt = false +ij_c_space_before_template_declaration_lt = false +ij_c_space_before_try_left_brace = true +ij_c_space_before_while_keyword = true +ij_c_space_before_while_left_brace = true +ij_c_space_before_while_parentheses = true +ij_c_space_between_adjacent_brackets = false +ij_c_space_between_operator_and_punctuator = false +ij_c_space_within_empty_array_initializer_braces = false +ij_c_spaces_around_additive_operators = true +ij_c_spaces_around_assignment_operators = true +ij_c_spaces_around_bitwise_operators = true +ij_c_spaces_around_equality_operators = true +ij_c_spaces_around_lambda_arrow = true +ij_c_spaces_around_logical_operators = true +ij_c_spaces_around_multiplicative_operators = true +ij_c_spaces_around_pm_operators = false +ij_c_spaces_around_relational_operators = true +ij_c_spaces_around_shift_operators = true +ij_c_spaces_around_unary_operator = false +ij_c_spaces_within_array_initializer_braces = false +ij_c_spaces_within_braces = true +ij_c_spaces_within_brackets = false +ij_c_spaces_within_cast_parentheses = false +ij_c_spaces_within_catch_parentheses = false +ij_c_spaces_within_category_parentheses = false +ij_c_spaces_within_empty_braces = false +ij_c_spaces_within_empty_function_call_parentheses = false +ij_c_spaces_within_empty_function_declaration_parentheses = false +ij_c_spaces_within_empty_lambda_capture_list_bracket = false +ij_c_spaces_within_empty_template_call_ltgt = false +ij_c_spaces_within_empty_template_declaration_ltgt = false +ij_c_spaces_within_for_parentheses = false +ij_c_spaces_within_function_call_parentheses = false +ij_c_spaces_within_function_declaration_parentheses = false +ij_c_spaces_within_if_parentheses = false +ij_c_spaces_within_lambda_capture_list_bracket = false +ij_c_spaces_within_method_parameter_type_parentheses = false +ij_c_spaces_within_method_return_type_parentheses = false +ij_c_spaces_within_parentheses = false +ij_c_spaces_within_property_attributes_parentheses = false +ij_c_spaces_within_protocols_brackets = false +ij_c_spaces_within_send_message_brackets = false +ij_c_spaces_within_switch_parentheses = false +ij_c_spaces_within_template_call_ltgt = false +ij_c_spaces_within_template_declaration_ltgt = false +ij_c_spaces_within_template_double_gt = true +ij_c_spaces_within_while_parentheses = false +ij_c_special_else_if_treatment = true +ij_c_superclass_list_after_colon = never +ij_c_superclass_list_align_multiline = true +ij_c_superclass_list_before_colon = if_long +ij_c_superclass_list_comma_on_next_line = false +ij_c_superclass_list_wrap = on_every_item +ij_c_tag_prefix_of_block_comment = at +ij_c_tag_prefix_of_line_comment = back_slash +ij_c_template_call_arguments_align_multiline = false +ij_c_template_call_arguments_align_multiline_pars = false +ij_c_template_call_arguments_comma_on_next_line = false +ij_c_template_call_arguments_new_line_after_lt = false +ij_c_template_call_arguments_new_line_before_gt = false +ij_c_template_call_arguments_wrap = off +ij_c_template_declaration_function_body_indent = false +ij_c_template_declaration_function_wrap = split_into_lines +ij_c_template_declaration_struct_body_indent = false +ij_c_template_declaration_struct_wrap = split_into_lines +ij_c_template_parameters_align_multiline = false +ij_c_template_parameters_align_multiline_pars = false +ij_c_template_parameters_comma_on_next_line = false +ij_c_template_parameters_new_line_after_lt = false +ij_c_template_parameters_new_line_before_gt = false +ij_c_template_parameters_wrap = off +ij_c_ternary_operation_signs_on_next_line = true +ij_c_ternary_operation_wrap = normal +ij_c_type_qualifiers_placement = before +ij_c_use_modern_casts = true +ij_c_use_setters_in_constructor = true +ij_c_while_brace_force = never +ij_c_while_on_new_line = false +ij_c_wrap_property_declaration = off + +[{*.cmake,CMakeLists.txt}] +ij_cmake_align_multiline_parameters_in_calls = false +ij_cmake_force_commands_case = 2 +ij_cmake_keep_blank_lines_in_code = 2 +ij_cmake_space_before_for_parentheses = true +ij_cmake_space_before_if_parentheses = true +ij_cmake_space_before_method_call_parentheses = false +ij_cmake_space_before_method_parentheses = false +ij_cmake_space_before_while_parentheses = true +ij_cmake_spaces_within_for_parentheses = false +ij_cmake_spaces_within_if_parentheses = false +ij_cmake_spaces_within_method_call_parentheses = false +ij_cmake_spaces_within_method_parentheses = false +ij_cmake_spaces_within_while_parentheses = false + +[{*.gant,*.gradle,*.groovy,*.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 5 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 3 +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_long_lines = false + +[{*.gradle.kts,*.kt,*.kts,*.main.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = off +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true +ij_kotlin_continuation_indent_in_elvis = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = off +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false +ij_kotlin_method_parameters_wrap = off +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.har,*.json}] +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.hcl,*.nomad}] +indent_size = 2 +ij_hcl_array_wrapping = 2 +ij_hcl_keep_blank_lines_in_code = 2 +ij_hcl_keep_indents_on_empty_lines = false +ij_hcl_keep_line_breaks = true +ij_hcl_object_wrapping = 2 +ij_hcl_property_alignment = 0 +ij_hcl_property_line_commenter_character = 0 +ij_hcl_space_after_comma = true +ij_hcl_space_before_comma = false +ij_hcl_spaces_around_assignment_operators = true +ij_hcl_spaces_within_braces = false +ij_hcl_spaces_within_brackets = false +ij_hcl_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = off +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = off +ij_html_uniform_ident = false + +[{*.tf,*.tfvars}] +indent_size = 2 +ij_hcl-terraform_array_wrapping = 2 +ij_hcl-terraform_keep_blank_lines_in_code = 2 +ij_hcl-terraform_keep_indents_on_empty_lines = false +ij_hcl-terraform_keep_line_breaks = true +ij_hcl-terraform_object_wrapping = 2 +ij_hcl-terraform_property_alignment = 0 +ij_hcl-terraform_property_line_commenter_character = 0 +ij_hcl-terraform_space_after_comma = true +ij_hcl-terraform_space_before_comma = false +ij_hcl-terraform_spaces_around_assignment_operators = true +ij_hcl-terraform_spaces_within_braces = false +ij_hcl-terraform_spaces_within_brackets = false +ij_hcl-terraform_wrap_long_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_space_before_colon = true +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/android/build.gradle b/android/build.gradle index a4ca7a421..6674cc771 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,46 +1,171 @@ -apply plugin: 'com.android.library' - -def safeExtGet(prop, fallback) { - rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback -} - -repositories { - mavenCentral() - jcenter() - google() -} - -buildscript { - repositories { - jcenter() - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - } -} - -android { - compileSdkVersion safeExtGet('compileSdkVersion', 28) - buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3') - defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', 16) - targetSdkVersion safeExtGet('targetSdkVersion', 28) - versionCode 1 - versionName "1.0" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - productFlavors { - } -} - -dependencies { - implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - //compile 'com.squareup.okhttp3:okhttp:+' - //{RNFetchBlob_PRE_0.28_DEPDENDENCY} -} +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' + +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + +repositories { + mavenCentral() + mavenLocal() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + maven { + // Android JSC is installed from npm + url "$rootDir/../node_modules/jsc-android/dist" + } + google() +} + +buildscript { + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. + // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies + if (project == rootProject) { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.2.1' + } + } +} + +def isNewArchitectureEnabled() { + return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" +} + +if (isNewArchitectureEnabled()) { + apply plugin: 'com.facebook.react' +} + +android { + def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION + if (agpVersion.tokenize('.')[0].toInteger() >= 7) { + namespace "com.ReactNativeBlobUtil" + buildFeatures { + buildConfig true + } + } + compileSdkVersion safeExtGet('compileSdkVersion', 30) + buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3') + defaultConfig { + minSdkVersion safeExtGet('minSdkVersion', 16) + targetSdkVersion safeExtGet('targetSdkVersion', 30) + versionCode 1 + versionName "1.0" + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lintOptions { + abortOnError false + } + productFlavors { + } + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += ['src/newarch'] + } else { + java.srcDirs += ['src/oldarch'] + } + } + } +} + +dependencies { + implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" + implementation 'org.apache.commons:commons-lang3:3.0' +} + +afterEvaluate { project -> + // do this only when building this lib alone as a root project + if (project != rootProject) { + return + } + + // some Gradle build hooks ref: + // https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html + task androidJavadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + classpath += files(android.libraryVariants.collect { variant -> + variant.javaCompileProvider.get().classpath.files + }) + } + + task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { + classifier = 'javadoc' + from androidJavadoc.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles + } + + android.libraryVariants.all { variant -> + def name = variant.name.capitalize() + def javaCompileTask = variant.javaCompileProvider.get() + + task "jar${name}"(type: Jar, dependsOn: javaCompileTask) { + from javaCompileTask.destinationDir + } + } + + artifacts { + archives androidSourcesJar + archives androidJavadocJar + } + + publishing { + publications { + mavenJava(MavenPublication) { + def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text) + + artifactId packageJson.name + groupId = "com.ReactNativeBlobUtil" + version = packageJson.version + + pom { + name = packageJson.title + description = packageJson.description + url = packageJson.repository.baseUrl + + licenses { + license { + name = packageJson.license + url = packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename + distribution = 'repo' + } + } + + developers { + developer { + id = packageJson.author.username + name = packageJson.author.name + } + } + } + } + } + repositories { + maven { + url = "file://${projectDir}/../android/maven" + } + } + } +} \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c75df059d..f6451390e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 01 10:33:07 BRT 2018 +#Fri Aug 07 22:58:34 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip diff --git a/android/gradlew b/android/gradlew old mode 100644 new mode 100755 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1c92420b8..336114d96 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,8 +1,4 @@ - - - - + @@ -21,10 +17,15 @@ - + + + + + + @@ -34,4 +35,4 @@ - + \ No newline at end of file diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java deleted file mode 100644 index 602d51d33..000000000 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java +++ /dev/null @@ -1,414 +0,0 @@ -package com.RNFetchBlob; - -import android.app.Activity; -import android.app.DownloadManager; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import androidx.core.content.FileProvider; -import android.util.SparseArray; - -import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; - -// Cookies -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.network.ForwardingCookieHandler; -import com.facebook.react.modules.network.CookieJarContainer; -import com.facebook.react.modules.network.OkHttpClientProvider; -import okhttp3.OkHttpClient; -import okhttp3.JavaNetCookieJar; - -import java.io.File; -import java.util.Map; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import static android.app.Activity.RESULT_OK; -import static com.RNFetchBlob.RNFetchBlobConst.GET_CONTENT_INTENT; - -public class RNFetchBlob extends ReactContextBaseJavaModule { - - private final OkHttpClient mClient; - - static ReactApplicationContext RCTContext; - private static LinkedBlockingQueue taskQueue = new LinkedBlockingQueue<>(); - private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 5000, TimeUnit.MILLISECONDS, taskQueue); - static LinkedBlockingQueue fsTaskQueue = new LinkedBlockingQueue<>(); - private static ThreadPoolExecutor fsThreadPool = new ThreadPoolExecutor(2, 10, 5000, TimeUnit.MILLISECONDS, taskQueue); - private static boolean ActionViewVisible = false; - private static SparseArray promiseTable = new SparseArray<>(); - - public RNFetchBlob(ReactApplicationContext reactContext) { - - super(reactContext); - - mClient = OkHttpClientProvider.getOkHttpClient(); - ForwardingCookieHandler mCookieHandler = new ForwardingCookieHandler(reactContext); - CookieJarContainer mCookieJarContainer = (CookieJarContainer) mClient.cookieJar(); - mCookieJarContainer.setCookieJar(new JavaNetCookieJar(mCookieHandler)); - - RCTContext = reactContext; - reactContext.addActivityEventListener(new ActivityEventListener() { - @Override - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - if(requestCode == GET_CONTENT_INTENT && resultCode == RESULT_OK) { - Uri d = data.getData(); - promiseTable.get(GET_CONTENT_INTENT).resolve(d.toString()); - promiseTable.remove(GET_CONTENT_INTENT); - } - } - - @Override - public void onNewIntent(Intent intent) { - - } - }); - } - - @Override - public String getName() { - return "RNFetchBlob"; - } - - @Override - public Map getConstants() { - return RNFetchBlobFS.getSystemfolders(this.getReactApplicationContext()); - } - - @ReactMethod - public void createFile(final String path, final String content, final String encode, final Promise promise) { - threadPool.execute(new Runnable() { - @Override - public void run() { - RNFetchBlobFS.createFile(path, content, encode, promise); - } - }); - } - - @ReactMethod - public void createFileASCII(final String path, final ReadableArray dataArray, final Promise promise) { - threadPool.execute(new Runnable() { - @Override - public void run() { - RNFetchBlobFS.createFileASCII(path, dataArray, promise); - } - }); - } - - @ReactMethod - public void actionViewIntent(String path, String mime, final Promise promise) { - try { - Uri uriForFile = FileProvider.getUriForFile(getCurrentActivity(), - this.getReactApplicationContext().getPackageName() + ".provider", new File(path)); - - if (Build.VERSION.SDK_INT >= 24) { - // Create the intent with data and type - Intent intent = new Intent(Intent.ACTION_VIEW) - .setDataAndType(uriForFile, mime); - - // Set flag to give temporary permission to external app to use FileProvider - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // All the activity to be opened outside of an activity - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - // Validate that the device can open the file - PackageManager pm = getCurrentActivity().getPackageManager(); - if (intent.resolveActivity(pm) != null) { - this.getReactApplicationContext().startActivity(intent); - } - - } else { - Intent intent = new Intent(Intent.ACTION_VIEW) - .setDataAndType(Uri.parse("file://" + path), mime).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - this.getReactApplicationContext().startActivity(intent); - } - ActionViewVisible = true; - - final LifecycleEventListener listener = new LifecycleEventListener() { - @Override - public void onHostResume() { - if(ActionViewVisible) - promise.resolve(null); - RCTContext.removeLifecycleEventListener(this); - } - - @Override - public void onHostPause() { - - } - - @Override - public void onHostDestroy() { - - } - }; - RCTContext.addLifecycleEventListener(listener); - } catch(Exception ex) { - promise.reject("EUNSPECIFIED", ex.getLocalizedMessage()); - } - } - - @ReactMethod - public void writeArrayChunk(final String streamId, final ReadableArray dataArray, final Callback callback) { - RNFetchBlobFS.writeArrayChunk(streamId, dataArray, callback); - } - - @ReactMethod - public void unlink(String path, Callback callback) { - RNFetchBlobFS.unlink(path, callback); - } - - @ReactMethod - public void mkdir(String path, Promise promise) { - RNFetchBlobFS.mkdir(path, promise); - } - - @ReactMethod - public void exists(String path, Callback callback) { - RNFetchBlobFS.exists(path, callback); - } - - @ReactMethod - public void cp(final String path, final String dest, final Callback callback) { - threadPool.execute(new Runnable() { - @Override - public void run() { - RNFetchBlobFS.cp(path, dest, callback); - } - }); - } - - @ReactMethod - public void mv(String path, String dest, Callback callback) { - RNFetchBlobFS.mv(path, dest, callback); - } - - @ReactMethod - public void ls(String path, Promise promise) { - RNFetchBlobFS.ls(path, promise); - } - - @ReactMethod - public void writeStream(String path, String encode, boolean append, Callback callback) { - new RNFetchBlobFS(this.getReactApplicationContext()).writeStream(path, encode, append, callback); - } - - @ReactMethod - public void writeChunk(String streamId, String data, Callback callback) { - RNFetchBlobFS.writeChunk(streamId, data, callback); - } - - @ReactMethod - public void closeStream(String streamId, Callback callback) { - RNFetchBlobFS.closeStream(streamId, callback); - } - - @ReactMethod - public void removeSession(ReadableArray paths, Callback callback) { - RNFetchBlobFS.removeSession(paths, callback); - } - - @ReactMethod - public void readFile(final String path, final String encoding, final Promise promise) { - threadPool.execute(new Runnable() { - @Override - public void run() { - RNFetchBlobFS.readFile(path, encoding, promise); - } - }); - } - - @ReactMethod - public void writeFileArray(final String path, final ReadableArray data, final boolean append, final Promise promise) { - threadPool.execute(new Runnable() { - @Override - public void run() { - RNFetchBlobFS.writeFile(path, data, append, promise); - } - }); - } - - @ReactMethod - public void writeFile(final String path, final String encoding, final String data, final boolean append, final Promise promise) { - threadPool.execute(new Runnable() { - @Override - public void run() { - RNFetchBlobFS.writeFile(path, encoding, data, append, promise); - } - }); - } - - @ReactMethod - public void lstat(String path, Callback callback) { - RNFetchBlobFS.lstat(path, callback); - } - - @ReactMethod - public void stat(String path, Callback callback) { - RNFetchBlobFS.stat(path, callback); - } - - @ReactMethod - public void scanFile(final ReadableArray pairs, final Callback callback) { - final ReactApplicationContext ctx = this.getReactApplicationContext(); - threadPool.execute(new Runnable() { - @Override - public void run() { - int size = pairs.size(); - String [] p = new String[size]; - String [] m = new String[size]; - for(int i=0;i createNativeModules(ReactApplicationContext reactContext) { - List modules = new ArrayList<>(); - modules.add(new RNFetchBlob(reactContext)); - return modules; - } - - public List> createJSModules() { - return Collections.emptyList(); - } - - @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); - } - -} diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java deleted file mode 100644 index ab35fdd31..000000000 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.RNFetchBlob; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; - -import java.security.MessageDigest; -import java.security.cert.CertificateException; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import okhttp3.OkHttpClient; - - -public class RNFetchBlobUtils { - - public static String getMD5(String input) { - String result = null; - - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - md.update(input.getBytes()); - byte[] digest = md.digest(); - - StringBuilder sb = new StringBuilder(); - - for (byte b : digest) { - sb.append(String.format("%02x", b & 0xff)); - } - - result = sb.toString(); - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - // TODO: Is discarding errors the intent? (https://www.owasp.org/index.php/Return_Inside_Finally_Block) - return result; - } - - } - - public static void emitWarningEvent(String data) { - WritableMap args = Arguments.createMap(); - args.putString("event", "warn"); - args.putString("detail", data); - - // emit event to js context - RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_MESSAGE, args); - } - - public static OkHttpClient.Builder getUnsafeOkHttpClient(OkHttpClient client) { - try { - // Create a trust manager that does not validate certificate chains - final X509TrustManager x509TrustManager = new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new java.security.cert.X509Certificate[]{}; - } - }; - final TrustManager[] trustAllCerts = new TrustManager[]{ x509TrustManager }; - - // Install the all-trusting trust manager - final SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - // Create an ssl socket factory with our all-trusting manager - final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - - OkHttpClient.Builder builder = client.newBuilder(); - builder.sslSocketFactory(sslSocketFactory, x509TrustManager); - builder.hostnameVerifier(new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }); - - return builder; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilBody.java similarity index 56% rename from android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java rename to android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilBody.java index adbe48b72..7990dd550 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilBody.java @@ -1,420 +1,450 @@ -package com.RNFetchBlob; - -import androidx.annotation.NonNull; -import android.util.Base64; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -import android.net.Uri; -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; - -class RNFetchBlobBody extends RequestBody{ - - private InputStream requestStream; - private long contentLength = 0; - private ReadableArray form; - private String mTaskId; - private String rawBody; - private RNFetchBlobReq.RequestType requestType; - private MediaType mime; - private File bodyCache; - int reported = 0; - private Boolean chunkedEncoding = false; - - RNFetchBlobBody(String taskId) { - this.mTaskId = taskId; - } - - RNFetchBlobBody chunkedEncoding(boolean val) { - this.chunkedEncoding = val; - return this; - } - - RNFetchBlobBody setMIME(MediaType mime) { - this.mime = mime; - return this; - } - - RNFetchBlobBody setRequestType(RNFetchBlobReq.RequestType type) { - this.requestType = type; - return this; - } - - /** - * Set request body - * @param body A string represents the request body - * @return object itself - */ - RNFetchBlobBody setBody(String body) { - this.rawBody = body; - if(rawBody == null) { - this.rawBody = ""; - requestType = RNFetchBlobReq.RequestType.AsIs; - } - try { - switch (requestType) { - case SingleFile: - requestStream = getRequestStream(); - contentLength = requestStream.available(); - break; - case AsIs: - contentLength = this.rawBody.getBytes().length; - requestStream = new ByteArrayInputStream(this.rawBody.getBytes()); - break; - case Others: - break; - } - } catch(Exception ex) { - ex.printStackTrace(); - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create single content request body :" + ex.getLocalizedMessage() + "\r\n"); - } - return this; - } - - /** - * Set request body (Array) - * @param body A Readable array contains form data - * @return object itself - */ - RNFetchBlobBody setBody(ReadableArray body) { - this.form = body; - try { - bodyCache = createMultipartBodyCache(); - requestStream = new FileInputStream(bodyCache); - contentLength = bodyCache.length(); - } catch(Exception ex) { - ex.printStackTrace(); - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create request multipart body :" + ex.getLocalizedMessage()); - } - return this; - } - - @Override - public long contentLength() { - return chunkedEncoding ? -1 : contentLength; - } - - @Override - public MediaType contentType() { - return mime; - } - - @Override - public void writeTo(@NonNull BufferedSink sink) { - try { - pipeStreamToSink(requestStream, sink); - } catch(Exception ex) { - RNFetchBlobUtils.emitWarningEvent(ex.getLocalizedMessage()); - ex.printStackTrace(); - } - } - - boolean clearRequestBody() { - try { - if (bodyCache != null && bodyCache.exists()) { - bodyCache.delete(); - } - } catch(Exception e) { - RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage()); - return false; - } - return true; - } - - private InputStream getRequestStream() throws Exception { - - // upload from storage - if (rawBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) { - String orgPath = rawBody.substring(RNFetchBlobConst.FILE_PREFIX.length()); - orgPath = RNFetchBlobFS.normalizePath(orgPath); - // upload file from assets - if (RNFetchBlobFS.isAsset(orgPath)) { - try { - String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); - return RNFetchBlob.RCTContext.getAssets().open(assetName); - } catch (Exception e) { - throw new Exception("error when getting request stream from asset : " +e.getLocalizedMessage()); - } - } else { - File f = new File(RNFetchBlobFS.normalizePath(orgPath)); - try { - if(!f.exists()) - f.createNewFile(); - return new FileInputStream(f); - } catch (Exception e) { - throw new Exception("error when getting request stream: " +e.getLocalizedMessage()); - } - } - } else if (rawBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { - String contentURI = rawBody.substring(RNFetchBlobConst.CONTENT_PREFIX.length()); - try { - return RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(contentURI)); - } catch (Exception e) { - throw new Exception("error when getting request stream for content URI: " + contentURI, e); - } - } - // base 64 encoded - else { - try { - byte[] bytes = Base64.decode(rawBody, 0); - return new ByteArrayInputStream(bytes); - } catch(Exception ex) { - throw new Exception("error when getting request stream: " + ex.getLocalizedMessage()); - } - } - } - - /** - * Create a temp file that contains content of multipart form data content - * @return The cache file object - * @throws IOException - */ - private File createMultipartBodyCache() throws IOException { - String boundary = "RNFetchBlob-" + mTaskId; - - File outputDir = RNFetchBlob.RCTContext.getCacheDir(); // context being the Activity pointer - File outputFile = File.createTempFile("rnfb-form-tmp", "", outputDir); - FileOutputStream os = new FileOutputStream(outputFile); - - ArrayList fields = countFormDataLength(); - ReactApplicationContext ctx = RNFetchBlob.RCTContext; - - for(FormField field : fields) { - String data = field.data; - String name = field.name; - // skip invalid fields - if(name == null || data == null) - continue; - // form begin - String header = "--" + boundary + "\r\n"; - if (field.filename != null) { - header += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + field.filename + "\"\r\n"; - header += "Content-Type: " + field.mime + "\r\n\r\n"; - os.write(header.getBytes()); - // file field header end - // upload from storage - if (data.startsWith(RNFetchBlobConst.FILE_PREFIX)) { - String orgPath = data.substring(RNFetchBlobConst.FILE_PREFIX.length()); - orgPath = RNFetchBlobFS.normalizePath(orgPath); - // path starts with content:// - if (RNFetchBlobFS.isAsset(orgPath)) { - try { - String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); - InputStream in = ctx.getAssets().open(assetName); - pipeStreamToFileStream(in, os); - } catch (IOException e) { - RNFetchBlobUtils.emitWarningEvent("Failed to create form data asset :" + orgPath + ", " + e.getLocalizedMessage() ); - } - } - // data from normal files - else { - File file = new File(RNFetchBlobFS.normalizePath(orgPath)); - if(file.exists()) { - FileInputStream fs = new FileInputStream(file); - pipeStreamToFileStream(fs, os); - } - else { - RNFetchBlobUtils.emitWarningEvent("Failed to create form data from path :" + orgPath + ", file not exists."); - } - } - } else if (data.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { - String contentURI = data.substring(RNFetchBlobConst.CONTENT_PREFIX.length()); - InputStream is = null; - try { - is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI)); - pipeStreamToFileStream(is, os); - } catch(Exception e) { - RNFetchBlobUtils.emitWarningEvent( - "Failed to create form data from content URI:" + contentURI + ", " + e.getLocalizedMessage()); - } finally { - if (is != null) { - is.close(); - } - } - } - // base64 embedded file content - else { - byte[] b = Base64.decode(data, 0); - os.write(b); - } - - } - // data field - else { - header += "Content-Disposition: form-data; name=\"" + name + "\"\r\n"; - header += "Content-Type: " + field.mime + "\r\n\r\n"; - os.write(header.getBytes()); - byte[] fieldData = field.data.getBytes(); - os.write(fieldData); - } - // form end - os.write("\r\n".getBytes()); - } - // close the form - byte[] end = ("--" + boundary + "--\r\n").getBytes(); - os.write(end); - os.flush(); - os.close(); - return outputFile; - } - - /** - * Pipe input stream to request body output stream - * @param stream The input stream - * @param sink The request body buffer sink - * @throws IOException - */ - private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws IOException { - byte[] chunk = new byte[10240]; - long totalWritten = 0; - int read; - while((read = stream.read(chunk, 0, 10240)) > 0) { - sink.write(chunk, 0, read); - totalWritten += read; - emitUploadProgress(totalWritten); - } - stream.close(); - } - - /** - * Pipe input stream to a file - * @param is The input stream - * @param os The output stream to a file - * @throws IOException - */ - private void pipeStreamToFileStream(InputStream is, FileOutputStream os) throws IOException { - - byte[] buf = new byte[10240]; - int len; - while ((len = is.read(buf)) > 0) { - os.write(buf, 0, len); - } - is.close(); - } - - /** - * Compute approximate content length for form data - * @return ArrayList - */ - private ArrayList countFormDataLength() throws IOException { - long total = 0; - ArrayList list = new ArrayList<>(); - ReactApplicationContext ctx = RNFetchBlob.RCTContext; - for(int i = 0;i < form.size(); i++) { - FormField field = new FormField(form.getMap(i)); - list.add(field); - if(field.data == null) { - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob multipart request builder has found a field without `data` property, the field `"+ field.name +"` will be removed implicitly."); - } - else if (field.filename != null) { - String data = field.data; - // upload from storage - if (data.startsWith(RNFetchBlobConst.FILE_PREFIX)) { - String orgPath = data.substring(RNFetchBlobConst.FILE_PREFIX.length()); - orgPath = RNFetchBlobFS.normalizePath(orgPath); - // path starts with asset:// - if (RNFetchBlobFS.isAsset(orgPath)) { - try { - String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); - long length = ctx.getAssets().open(assetName).available(); - total += length; - } catch (IOException e) { - RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage()); - } - } - // general files - else { - File file = new File(RNFetchBlobFS.normalizePath(orgPath)); - total += file.length(); - } - } else if (data.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { - String contentURI = data.substring(RNFetchBlobConst.CONTENT_PREFIX.length()); - InputStream is = null; - try { - is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI)); - long length = is.available(); - total += length; - } catch(Exception e) { - RNFetchBlobUtils.emitWarningEvent( - "Failed to estimate form data length from content URI:" + contentURI + ", " + e.getLocalizedMessage()); - } finally { - if (is != null) { - is.close(); - } - } - } - // base64 embedded file content - else { - byte[] bytes = Base64.decode(data, 0); - total += bytes.length; - } - } - // data field - else { - total += field.data.getBytes().length; - } - } - contentLength = total; - return list; - } - - /** - * Since ReadableMap could only be access once, we have to store the field into a map for - * repeatedly access. - */ - private class FormField { - public String name; - String filename; - String mime; - public String data; - - FormField(ReadableMap rawData) { - if(rawData.hasKey("name")) - name = rawData.getString("name"); - if(rawData.hasKey("filename")) - filename = rawData.getString("filename"); - if(rawData.hasKey("type")) - mime = rawData.getString("type"); - else { - mime = filename == null ? "text/plain" : "application/octet-stream"; - } - if(rawData.hasKey("data")) { - data = rawData.getString("data"); - } - } - } - - /** - * Emit progress event - * @param written Integer - */ - private void emitUploadProgress(long written) { - RNFetchBlobProgressConfig config = RNFetchBlobReq.getReportUploadProgress(mTaskId); - if(config != null && contentLength != 0 && config.shouldReport((float)written/contentLength)) { - WritableMap args = Arguments.createMap(); - args.putString("taskId", mTaskId); - args.putString("written", String.valueOf(written)); - args.putString("total", String.valueOf(contentLength)); - - // emit event to js context - RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args); - } - } - -} +package com.ReactNativeBlobUtil; + +import android.net.Uri; +import android.util.Base64; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; + +class ReactNativeBlobUtilBody extends RequestBody { + + private long contentLength = 0; + private ReadableArray form; + private String mTaskId; + private String rawBody; + private ReactNativeBlobUtilReq.RequestType requestType; + private MediaType mime; + private File bodyCache; + int reported = 0; + private Boolean chunkedEncoding = false; + + ReactNativeBlobUtilBody(String taskId) { + this.mTaskId = taskId; + } + + ReactNativeBlobUtilBody chunkedEncoding(boolean val) { + this.chunkedEncoding = val; + return this; + } + + ReactNativeBlobUtilBody setMIME(MediaType mime) { + this.mime = mime; + return this; + } + + ReactNativeBlobUtilBody setRequestType(ReactNativeBlobUtilReq.RequestType type) { + this.requestType = type; + return this; + } + + /** + * Set request body + * + * @param body A string represents the request body + * @return object itself + */ + ReactNativeBlobUtilBody setBody(String body) { + this.rawBody = body; + if (rawBody == null) { + this.rawBody = ""; + requestType = ReactNativeBlobUtilReq.RequestType.AsIs; + } + try { + switch (requestType) { + case SingleFile: + contentLength = getRequestStream().available(); + break; + case AsIs: + contentLength = this.rawBody.getBytes().length; + break; + case Others: + break; + } + } catch (Exception ex) { + ex.printStackTrace(); + ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil failed to create single content request body :" + ex.getLocalizedMessage() + "\r\n"); + } + return this; + } + + /** + * Set request body (Array) + * + * @param body A Readable array contains form data + * @return object itself + */ + ReactNativeBlobUtilBody setBody(ReadableArray body) { + this.form = body; + try { + bodyCache = createMultipartBodyCache(); + contentLength = bodyCache.length(); + } catch (Exception ex) { + ex.printStackTrace(); + ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil failed to create request multipart body :" + ex.getLocalizedMessage()); + } + return this; + } + + // This organizes the input stream initialization logic into a method. This allows: + // 1) Initialization to be deferred until it's needed (when we are ready to pipe it into the BufferedSink) + // 2) The stream to be initialized and used as many times as necessary. When okhttp runs into + // a connection error, it will retry the request which will require a new stream to write into + // the sink once again. + InputStream getInputStreamForRequestBody() { + try { + if (this.form != null) { + return new FileInputStream(bodyCache); + } else { + switch (requestType) { + case SingleFile: + return getRequestStream(); + case AsIs: + return new ByteArrayInputStream(this.rawBody.getBytes()); + case Others: + ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil could not create input stream for request type others"); + break; + } + } + } catch (Exception ex){ + ex.printStackTrace(); + ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil failed to create input stream for request:" + ex.getLocalizedMessage()); + } + + return null; + } + + @Override + public long contentLength() { + return chunkedEncoding ? -1 : contentLength; + } + + @Override + public MediaType contentType() { + return mime; + } + + @Override + public void writeTo(@NonNull BufferedSink sink) { + try { + pipeStreamToSink(getInputStreamForRequestBody(), sink); + } catch (Exception ex) { + ReactNativeBlobUtilUtils.emitWarningEvent(ex.getLocalizedMessage()); + ex.printStackTrace(); + } + } + + boolean clearRequestBody() { + try { + if (bodyCache != null && bodyCache.exists()) { + bodyCache.delete(); + } + } catch (Exception e) { + ReactNativeBlobUtilUtils.emitWarningEvent(e.getLocalizedMessage()); + return false; + } + return true; + } + + private InputStream getRequestStream() throws Exception { + + // upload from storage + if (rawBody.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX)) { + String orgPath = rawBody.substring(ReactNativeBlobUtilConst.FILE_PREFIX.length()); + orgPath = ReactNativeBlobUtilUtils.normalizePath(orgPath); + // upload file from assets + if (ReactNativeBlobUtilUtils.isAsset(orgPath)) { + try { + String assetName = orgPath.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, ""); + return ReactNativeBlobUtilImpl.RCTContext.getAssets().open(assetName); + } catch (Exception e) { + throw new Exception("error when getting request stream from asset : " + e.getLocalizedMessage()); + } + } else { + File f = new File(ReactNativeBlobUtilUtils.normalizePath(orgPath)); + try { + if (!f.exists()) + f.createNewFile(); + return new FileInputStream(f); + } catch (Exception e) { + throw new Exception("error when getting request stream: " + e.getLocalizedMessage()); + } + } + } else if (rawBody.startsWith(ReactNativeBlobUtilConst.CONTENT_PREFIX)) { + String contentURI = rawBody.substring(ReactNativeBlobUtilConst.CONTENT_PREFIX.length()); + try { + return ReactNativeBlobUtilImpl.RCTContext.getContentResolver().openInputStream(Uri.parse(contentURI)); + } catch (Exception e) { + throw new Exception("error when getting request stream for content URI: " + contentURI, e); + } + } + // base 64 encoded + else { + try { + byte[] bytes = Base64.decode(rawBody, 0); + return new ByteArrayInputStream(bytes); + } catch (Exception ex) { + throw new Exception("error when getting request stream: " + ex.getLocalizedMessage()); + } + } + } + + /** + * Create a temp file that contains content of multipart form data content + * + * @return The cache file object + * @throws IOException . + */ + private File createMultipartBodyCache() throws IOException { + String boundary = "ReactNativeBlobUtil-" + mTaskId; + + File outputDir = ReactNativeBlobUtilImpl.RCTContext.getCacheDir(); // context being the Activity pointer + File outputFile = File.createTempFile("rnfb-form-tmp", "", outputDir); + FileOutputStream os = new FileOutputStream(outputFile); + + ArrayList fields = countFormDataLength(); + ReactApplicationContext ctx = ReactNativeBlobUtilImpl.RCTContext; + + for (FormField field : fields) { + String data = field.data; + String name = field.name; + // skip invalid fields + if (name == null || data == null) + continue; + // form begin + String header = "--" + boundary + "\r\n"; + if (field.filename != null) { + header += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + field.filename + "\"\r\n"; + header += "Content-Type: " + field.mime + "\r\n\r\n"; + os.write(header.getBytes()); + // file field header end + // upload from storage + if (data.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX)) { + String orgPath = data.substring(ReactNativeBlobUtilConst.FILE_PREFIX.length()); + orgPath = ReactNativeBlobUtilUtils.normalizePath(orgPath); + // path starts with content:// + if (ReactNativeBlobUtilUtils.isAsset(orgPath)) { + try { + String assetName = orgPath.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, ""); + InputStream in = ctx.getAssets().open(assetName); + pipeStreamToFileStream(in, os); + } catch (IOException e) { + ReactNativeBlobUtilUtils.emitWarningEvent("Failed to create form data asset :" + orgPath + ", " + e.getLocalizedMessage()); + } + } + // data from normal files + else { + File file = new File(ReactNativeBlobUtilUtils.normalizePath(orgPath)); + if (file.exists()) { + FileInputStream fs = new FileInputStream(file); + pipeStreamToFileStream(fs, os); + } else { + ReactNativeBlobUtilUtils.emitWarningEvent("Failed to create form data from path :" + orgPath + ", file not exists."); + } + } + } else if (data.startsWith(ReactNativeBlobUtilConst.CONTENT_PREFIX)) { + String contentURI = data.substring(ReactNativeBlobUtilConst.CONTENT_PREFIX.length()); + InputStream is = null; + try { + is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI)); + pipeStreamToFileStream(is, os); + } catch (Exception e) { + ReactNativeBlobUtilUtils.emitWarningEvent( + "Failed to create form data from content URI:" + contentURI + ", " + e.getLocalizedMessage()); + } finally { + if (is != null) { + is.close(); + } + } + } + // base64 embedded file content + else { + byte[] b = Base64.decode(data, 0); + os.write(b); + } + + } + // data field + else { + header += "Content-Disposition: form-data; name=\"" + name + "\"\r\n"; + header += "Content-Type: " + field.mime + "\r\n\r\n"; + os.write(header.getBytes()); + byte[] fieldData = field.data.getBytes(); + os.write(fieldData); + } + // form end + os.write("\r\n".getBytes()); + } + // close the form + byte[] end = ("--" + boundary + "--\r\n").getBytes(); + os.write(end); + os.flush(); + os.close(); + return outputFile; + } + + /** + * Pipe input stream to request body output stream + * + * @param stream The input stream + * @param sink The request body buffer sink + * @throws IOException . + */ + private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws IOException { + byte[] chunk = new byte[10240]; + long totalWritten = 0; + int read; + while ((read = stream.read(chunk, 0, 10240)) > 0) { + sink.write(chunk, 0, read); + totalWritten += read; + emitUploadProgress(totalWritten); + } + stream.close(); + } + + /** + * Pipe input stream to a file + * + * @param is The input stream + * @param os The output stream to a file + * @throws IOException + */ + private void pipeStreamToFileStream(InputStream is, FileOutputStream os) throws IOException { + + byte[] buf = new byte[10240]; + int len; + while ((len = is.read(buf)) > 0) { + os.write(buf, 0, len); + } + is.close(); + } + + /** + * Compute approximate content length for form data + * + * @return ArrayList + */ + private ArrayList countFormDataLength() throws IOException { + long total = 0; + ArrayList list = new ArrayList<>(); + ReactApplicationContext ctx = ReactNativeBlobUtilImpl.RCTContext; + for (int i = 0; i < form.size(); i++) { + FormField field = new FormField(form.getMap(i)); + list.add(field); + if (field.data == null) { + ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil multipart request builder has found a field without `data` property, the field `" + field.name + "` will be removed implicitly."); + } else if (field.filename != null) { + String data = field.data; + // upload from storage + if (data.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX)) { + String orgPath = data.substring(ReactNativeBlobUtilConst.FILE_PREFIX.length()); + orgPath = ReactNativeBlobUtilUtils.normalizePath(orgPath); + // path starts with asset:// + if (ReactNativeBlobUtilUtils.isAsset(orgPath)) { + try { + String assetName = orgPath.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, ""); + long length = ctx.getAssets().open(assetName).available(); + total += length; + } catch (IOException e) { + ReactNativeBlobUtilUtils.emitWarningEvent(e.getLocalizedMessage()); + } + } + // general files + else { + File file = new File(ReactNativeBlobUtilUtils.normalizePath(orgPath)); + total += file.length(); + } + } else if (data.startsWith(ReactNativeBlobUtilConst.CONTENT_PREFIX)) { + String contentURI = data.substring(ReactNativeBlobUtilConst.CONTENT_PREFIX.length()); + InputStream is = null; + try { + is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI)); + long length = is.available(); + total += length; + } catch (Exception e) { + ReactNativeBlobUtilUtils.emitWarningEvent( + "Failed to estimate form data length from content URI:" + contentURI + ", " + e.getLocalizedMessage()); + } finally { + if (is != null) { + is.close(); + } + } + } + // base64 embedded file content + else { + byte[] bytes = Base64.decode(data, 0); + total += bytes.length; + } + } + // data field + else { + total += field.data.getBytes().length; + } + } + contentLength = total; + return list; + } + + /** + * Since ReadableMap could only be access once, we have to store the field into a map for + * repeatedly access. + */ + private class FormField { + public String name; + String filename; + String mime; + public String data; + + FormField(ReadableMap rawData) { + if (rawData.hasKey("name")) + name = rawData.getString("name"); + if (rawData.hasKey("filename")) + filename = rawData.getString("filename"); + if (rawData.hasKey("type")) + mime = rawData.getString("type"); + else { + mime = filename == null ? "text/plain" : "application/octet-stream"; + } + if (rawData.hasKey("data")) { + data = rawData.getString("data"); + } + } + } + + /** + * Emit progress event + * + * @param written Integer + */ + private void emitUploadProgress(long written) { + ReactNativeBlobUtilProgressConfig config = ReactNativeBlobUtilReq.getReportUploadProgress(mTaskId); + if (config != null && contentLength != 0 && config.shouldReport((float) written / contentLength)) { + WritableMap args = Arguments.createMap(); + args.putString("taskId", mTaskId); + args.putString("written", String.valueOf(written)); + args.putString("total", String.valueOf(contentLength)); + + // emit event to js context + ReactNativeBlobUtilImpl.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(ReactNativeBlobUtilConst.EVENT_UPLOAD_PROGRESS, args); + } + } + +} diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConfig.java similarity index 56% rename from android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java rename to android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConfig.java index 8ac9e7a85..922dd76fe 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConfig.java @@ -1,11 +1,14 @@ -package com.RNFetchBlob; +package com.ReactNativeBlobUtil; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -class RNFetchBlobConfig { +import java.util.Locale; + +class ReactNativeBlobUtilConfig { public Boolean fileCache; + public Boolean transformFile; public String path; public String appendExt; public ReadableMap addAndroidDownloads; @@ -20,32 +23,33 @@ class RNFetchBlobConfig { public Boolean followRedirect = true; public ReadableArray binaryContentTypes = null; - RNFetchBlobConfig(ReadableMap options) { - if(options == null) + ReactNativeBlobUtilConfig(ReadableMap options) { + if (options == null) return; - this.fileCache = options.hasKey("fileCache") ? options.getBoolean("fileCache") : false; + this.fileCache = options.hasKey("fileCache") && options.getBoolean("fileCache"); + this.transformFile = options.hasKey("transformFile") ? options.getBoolean("transformFile") : false; this.path = options.hasKey("path") ? options.getString("path") : null; this.appendExt = options.hasKey("appendExt") ? options.getString("appendExt") : ""; - this.trusty = options.hasKey("trusty") ? options.getBoolean("trusty") : false; - this.wifiOnly = options.hasKey("wifiOnly") ? options.getBoolean("wifiOnly") : false; - if(options.hasKey("addAndroidDownloads")) { + this.trusty = options.hasKey("trusty") && options.getBoolean("trusty"); + this.wifiOnly = options.hasKey("wifiOnly") && options.getBoolean("wifiOnly"); + if (options.hasKey("addAndroidDownloads")) { this.addAndroidDownloads = options.getMap("addAndroidDownloads"); } - if(options.hasKey("binaryContentTypes")) + if (options.hasKey("binaryContentTypes")) this.binaryContentTypes = options.getArray("binaryContentTypes"); - if(this.path != null && path.toLowerCase().contains("?append=true")) { + if (this.path != null && path.toLowerCase(Locale.ROOT).contains("?append=true")) { this.overwrite = false; } - if(options.hasKey("overwrite")) + if (options.hasKey("overwrite")) this.overwrite = options.getBoolean("overwrite"); - if(options.hasKey("followRedirect")) { + if (options.hasKey("followRedirect")) { this.followRedirect = options.getBoolean("followRedirect"); } this.key = options.hasKey("key") ? options.getString("key") : null; this.mime = options.hasKey("contentType") ? options.getString("contentType") : null; - this.increment = options.hasKey("increment") ? options.getBoolean("increment") : false; - this.auto = options.hasKey("auto") ? options.getBoolean("auto") : false; - if(options.hasKey("timeout")) { + this.increment = options.hasKey("increment") && options.getBoolean("increment"); + this.auto = options.hasKey("auto") && options.getBoolean("auto"); + if (options.hasKey("timeout")) { this.timeout = options.getInt("timeout"); } } diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConst.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConst.java new file mode 100644 index 000000000..f4f4490ae --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConst.java @@ -0,0 +1,20 @@ +package com.ReactNativeBlobUtil; + + +public class ReactNativeBlobUtilConst { + public static final String EVENT_UPLOAD_PROGRESS = "ReactNativeBlobUtilProgress-upload"; + public static final String EVENT_PROGRESS = "ReactNativeBlobUtilProgress"; + public static final String EVENT_HTTP_STATE = "ReactNativeBlobUtilState"; + public static final String EVENT_MESSAGE = "ReactNativeBlobUtilMessage"; + public static final String EVENT_FILESYSTEM = "ReactNativeBlobUtilFilesystem"; + public static final String FILE_PREFIX = "ReactNativeBlobUtil-file://"; + public static final String CONTENT_PREFIX = "ReactNativeBlobUtil-content://"; + public static final String FILE_PREFIX_BUNDLE_ASSET = "bundle-assets://"; + public static final String FILE_PREFIX_CONTENT = "content://"; + public static final String DATA_ENCODE_URI = "uri"; + public static final String RNFB_RESPONSE_BASE64 = "base64"; + public static final String RNFB_RESPONSE_UTF8 = "utf8"; + public static final String RNFB_RESPONSE_PATH = "path"; + public static final Integer GET_CONTENT_INTENT = 99900; + +} diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFS.java similarity index 55% rename from android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java rename to android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFS.java index a4d70153f..c3991da0a 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFS.java @@ -1,1153 +1,1058 @@ -package com.RNFetchBlob; - -import android.content.res.AssetFileDescriptor; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Environment; -import android.os.StatFs; -import android.os.SystemClock; -import android.util.Base64; - -import com.RNFetchBlob.Utils.PathResolver; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; - -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -class RNFetchBlobFS { - - private ReactApplicationContext mCtx; - private DeviceEventManagerModule.RCTDeviceEventEmitter emitter; - private String encoding = "base64"; - private OutputStream writeStreamInstance = null; - private static HashMap fileStreams = new HashMap<>(); - - RNFetchBlobFS(ReactApplicationContext ctx) { - this.mCtx = ctx; - this.emitter = ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - } - - /** - * Write string with encoding to file - * @param path Destination file path. - * @param encoding Encoding of the string. - * @param data Array passed from JS context. - * @param promise RCT Promise - */ - static void writeFile(String path, String encoding, String data, final boolean append, final Promise promise) { - try { - int written; - File f = new File(path); - File dir = f.getParentFile(); - - if(!f.exists()) { - if(dir != null && !dir.exists()) { - if (!dir.mkdirs()) { - promise.reject("EUNSPECIFIED", "Failed to create parent directory of '" + path + "'"); - return; - } - } - if(!f.createNewFile()) { - promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created"); - return; - } - } - - // write data from a file - if(encoding.equalsIgnoreCase(RNFetchBlobConst.DATA_ENCODE_URI)) { - String normalizedData = normalizePath(data); - File src = new File(normalizedData); - if (!src.exists()) { - promise.reject("ENOENT", "No such file '" + path + "' " + "('" + normalizedData + "')"); - return; - } - byte[] buffer = new byte [10240]; - int read; - written = 0; - FileInputStream fin = null; - FileOutputStream fout = null; - try { - fin = new FileInputStream(src); - fout = new FileOutputStream(f, append); - while ((read = fin.read(buffer)) > 0) { - fout.write(buffer, 0, read); - written += read; - } - } finally { - if (fin != null) { - fin.close(); - } - if (fout != null) { - fout.close(); - } - } - } - else { - byte[] bytes = stringToBytes(data, encoding); - FileOutputStream fout = new FileOutputStream(f, append); - try { - fout.write(bytes); - written = bytes.length; - } finally { - fout.close(); - } - } - promise.resolve(written); - } catch (FileNotFoundException e) { - // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html - promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created, or it is a directory"); - } catch (Exception e) { - promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); - } - } - - /** - * Write array of bytes into file - * @param path Destination file path. - * @param data Array passed from JS context. - * @param promise RCT Promise - */ - static void writeFile(String path, ReadableArray data, final boolean append, final Promise promise) { - try { - File f = new File(path); - File dir = f.getParentFile(); - - if(!f.exists()) { - if(dir != null && !dir.exists()) { - if (!dir.mkdirs()) { - promise.reject("ENOTDIR", "Failed to create parent directory of '" + path + "'"); - return; - } - } - if(!f.createNewFile()) { - promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created"); - return; - } - } - - FileOutputStream os = new FileOutputStream(f, append); - try { - byte[] bytes = new byte[data.size()]; - for (int i = 0; i < data.size(); i++) { - bytes[i] = (byte) data.getInt(i); - } - os.write(bytes); - } finally { - os.close(); - } - promise.resolve(data.size()); - } catch (FileNotFoundException e) { - // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html - promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created"); - } catch (Exception e) { - promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); - } - } - - /** - * Read file with a buffer that has the same size as the target file. - * @param path Path of the file. - * @param encoding Encoding of read stream. - * @param promise JS promise - */ - static void readFile(String path, String encoding, final Promise promise) { - String resolved = normalizePath(path); - if(resolved != null) - path = resolved; - try { - byte[] bytes; - int bytesRead; - int length; // max. array length limited to "int", also see https://stackoverflow.com/a/10787175/544779 - - if(resolved != null && resolved.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { - String assetName = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); - // This fails should an asset file be >2GB - length = (int) RNFetchBlob.RCTContext.getAssets().openFd(assetName).getLength(); - bytes = new byte[length]; - InputStream in = RNFetchBlob.RCTContext.getAssets().open(assetName); - bytesRead = in.read(bytes, 0, length); - in.close(); - } - // issue 287 - else if(resolved == null) { - InputStream in = RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); - // TODO See https://developer.android.com/reference/java/io/InputStream.html#available() - // Quote: "Note that while some implementations of InputStream will return the total number of bytes - // in the stream, many will not. It is never correct to use the return value of this method to - // allocate a buffer intended to hold all data in this stream." - length = in.available(); - bytes = new byte[length]; - bytesRead = in.read(bytes); - in.close(); - } - else { - File f = new File(path); - length = (int) f.length(); - bytes = new byte[length]; - FileInputStream in = new FileInputStream(f); - bytesRead = in.read(bytes); - in.close(); - } - - if (bytesRead < length) { - promise.reject("EUNSPECIFIED", "Read only " + bytesRead + " bytes of " + length); - return; - } - - switch (encoding.toLowerCase()) { - case "base64" : - promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP)); - break; - case "ascii" : - WritableArray asciiResult = Arguments.createArray(); - for (byte b : bytes) { - asciiResult.pushInt((int) b); - } - promise.resolve(asciiResult); - break; - case "utf8" : - promise.resolve(new String(bytes)); - break; - default: - promise.resolve(new String(bytes)); - break; - } - } - catch(FileNotFoundException err) { - String msg = err.getLocalizedMessage(); - if (msg.contains("EISDIR")) { - promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory; " + msg); - } else { - promise.reject("ENOENT", "No such file '" + path + "'; " + msg); - } - } - catch(Exception err) { - promise.reject("EUNSPECIFIED", err.getLocalizedMessage()); - } - - } - - /** - * Static method that returns system folders to JS context - * @param ctx React Native application context - */ - static Map getSystemfolders(ReactApplicationContext ctx) { - Map res = new HashMap<>(); - - res.put("DocumentDir", ctx.getFilesDir().getAbsolutePath()); - res.put("CacheDir", ctx.getCacheDir().getAbsolutePath()); - res.put("DCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()); - res.put("PictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); - res.put("MusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath()); - res.put("DownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()); - res.put("MovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath()); - res.put("RingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath()); - String state; - state = Environment.getExternalStorageState(); - if (state.equals(Environment.MEDIA_MOUNTED)) { - res.put("SDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath()); - - File externalDirectory = ctx.getExternalFilesDir(null); - - if (externalDirectory != null) { - res.put("SDCardApplicationDir", externalDirectory.getParentFile().getAbsolutePath()); - } else { - res.put("SDCardApplicationDir", ""); - } - } - res.put("MainBundleDir", ctx.getApplicationInfo().dataDir); - - return res; - } - - static public void getSDCardDir(Promise promise) { - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - promise.resolve(Environment.getExternalStorageDirectory().getAbsolutePath()); - } else { - promise.reject("RNFetchBlob.getSDCardDir", "External storage not mounted"); - } - - } - - static public void getSDCardApplicationDir(ReactApplicationContext ctx, Promise promise) { - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - try { - final String path = ctx.getExternalFilesDir(null).getParentFile().getAbsolutePath(); - promise.resolve(path); - } catch (Exception e) { - promise.reject("RNFetchBlob.getSDCardApplicationDir", e.getLocalizedMessage()); - } - } else { - promise.reject("RNFetchBlob.getSDCardApplicationDir", "External storage not mounted"); - } - } - - /** - * Static method that returns a temp file path - * @param taskId An unique string for identify - * @return String - */ - static String getTmpPath(String taskId) { - return RNFetchBlob.RCTContext.getFilesDir() + "/RNFetchBlobTmp_" + taskId; - } - - /** - * Create a file stream for read - * @param path File stream target path - * @param encoding File stream decoder, should be one of `base64`, `utf8`, `ascii` - * @param bufferSize Buffer size of read stream, default to 4096 (4095 when encode is `base64`) - */ - void readStream(String path, String encoding, int bufferSize, int tick, final String streamId) { - String resolved = normalizePath(path); - if(resolved != null) - path = resolved; - - try { - int chunkSize = encoding.equalsIgnoreCase("base64") ? 4095 : 4096; - if(bufferSize > 0) - chunkSize = bufferSize; - - InputStream fs; - - if(resolved != null && path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { - fs = RNFetchBlob.RCTContext.getAssets().open(path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "")); - } - // fix issue 287 - else if(resolved == null) { - fs = RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); - } - else { - fs = new FileInputStream(new File(path)); - } - - byte[] buffer = new byte[chunkSize]; - int cursor = 0; - boolean error = false; - - if (encoding.equalsIgnoreCase("utf8")) { - CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); - while ((cursor = fs.read(buffer)) != -1) { - encoder.encode(ByteBuffer.wrap(buffer).asCharBuffer()); - String chunk = new String(buffer, 0, cursor); - emitStreamEvent(streamId, "data", chunk); - if(tick > 0) - SystemClock.sleep(tick); - } - } else if (encoding.equalsIgnoreCase("ascii")) { - while ((cursor = fs.read(buffer)) != -1) { - WritableArray chunk = Arguments.createArray(); - for(int i =0;i 0) - SystemClock.sleep(tick); - } - } else if (encoding.equalsIgnoreCase("base64")) { - while ((cursor = fs.read(buffer)) != -1) { - if(cursor < chunkSize) { - byte[] copy = new byte[cursor]; - System.arraycopy(buffer, 0, copy, 0, cursor); - emitStreamEvent(streamId, "data", Base64.encodeToString(copy, Base64.NO_WRAP)); - } - else - emitStreamEvent(streamId, "data", Base64.encodeToString(buffer, Base64.NO_WRAP)); - if(tick > 0) - SystemClock.sleep(tick); - } - } else { - emitStreamEvent( - streamId, - "error", - "EINVAL", - "Unrecognized encoding `" + encoding + "`, should be one of `base64`, `utf8`, `ascii`" - ); - error = true; - } - - if(!error) - emitStreamEvent(streamId, "end", ""); - fs.close(); - buffer = null; - } catch (FileNotFoundException err) { - emitStreamEvent( - streamId, - "error", - "ENOENT", - "No such file '" + path + "'" - ); - } catch (Exception err) { - emitStreamEvent( - streamId, - "error", - "EUNSPECIFIED", - "Failed to convert data to " + encoding + " encoded string. This might be because this encoding cannot be used for this data." - ); - err.printStackTrace(); - } - } - - /** - * Create a write stream and store its instance in RNFetchBlobFS.fileStreams - * @param path Target file path - * @param encoding Should be one of `base64`, `utf8`, `ascii` - * @param append Flag represents if the file stream overwrite existing content - * @param callback Callback - */ - void writeStream(String path, String encoding, boolean append, Callback callback) { - try { - File dest = new File(path); - File dir = dest.getParentFile(); - - if(!dest.exists()) { - if(dir != null && !dir.exists()) { - if (!dir.mkdirs()) { - callback.invoke("ENOTDIR", "Failed to create parent directory of '" + path + "'"); - return; - } - } - if(!dest.createNewFile()) { - callback.invoke("ENOENT", "File '" + path + "' does not exist and could not be created"); - return; - } - } else if(dest.isDirectory()) { - callback.invoke("EISDIR", "Expecting a file but '" + path + "' is a directory"); - return; - } - - OutputStream fs = new FileOutputStream(path, append); - this.encoding = encoding; - String streamId = UUID.randomUUID().toString(); - RNFetchBlobFS.fileStreams.put(streamId, this); - this.writeStreamInstance = fs; - callback.invoke(null, null, streamId); - } catch(Exception err) { - callback.invoke("EUNSPECIFIED", "Failed to create write stream at path `" + path + "`; " + err.getLocalizedMessage()); - } - } - - /** - * Write a chunk of data into a file stream. - * @param streamId File stream ID - * @param data Data chunk in string format - * @param callback JS context callback - */ - static void writeChunk(String streamId, String data, Callback callback) { - RNFetchBlobFS fs = fileStreams.get(streamId); - OutputStream stream = fs.writeStreamInstance; - byte[] chunk = RNFetchBlobFS.stringToBytes(data, fs.encoding); - try { - stream.write(chunk); - callback.invoke(); - } catch (Exception e) { - callback.invoke(e.getLocalizedMessage()); - } - } - - /** - * Write data using ascii array - * @param streamId File stream ID - * @param data Data chunk in ascii array format - * @param callback JS context callback - */ - static void writeArrayChunk(String streamId, ReadableArray data, Callback callback) { - try { - RNFetchBlobFS fs = fileStreams.get(streamId); - OutputStream stream = fs.writeStreamInstance; - byte[] chunk = new byte[data.size()]; - for(int i =0; i< data.size();i++) { - chunk[i] = (byte) data.getInt(i); - } - stream.write(chunk); - callback.invoke(); - } catch (Exception e) { - callback.invoke(e.getLocalizedMessage()); - } - } - - /** - * Close file write stream by ID - * @param streamId Stream ID - * @param callback JS context callback - */ - static void closeStream(String streamId, Callback callback) { - try { - RNFetchBlobFS fs = fileStreams.get(streamId); - OutputStream stream = fs.writeStreamInstance; - fileStreams.remove(streamId); - stream.close(); - callback.invoke(); - } catch(Exception err) { - callback.invoke(err.getLocalizedMessage()); - } - } - - /** - * Unlink file at path - * @param path Path of target - * @param callback JS context callback - */ - static void unlink(String path, Callback callback) { - try { - String normalizedPath = normalizePath(path); - RNFetchBlobFS.deleteRecursive(new File(normalizedPath)); - callback.invoke(null, true); - } catch(Exception err) { - callback.invoke(err.getLocalizedMessage(), false); - } - } - - private static void deleteRecursive(File fileOrDirectory) throws IOException { - if (fileOrDirectory.isDirectory()) { - File[] files = fileOrDirectory.listFiles(); - if (files == null) { - throw new NullPointerException("Received null trying to list files of directory '" + fileOrDirectory + "'"); - } else { - for (File child : files) { - deleteRecursive(child); - } - } - } - boolean result = fileOrDirectory.delete(); - if (!result) { - throw new IOException("Failed to delete '" + fileOrDirectory + "'"); - } - } - - /** - * Make a folder - * @param path Source path - * @param promise JS promise - */ - static void mkdir(String path, Promise promise) { - File dest = new File(path); - if(dest.exists()) { - promise.reject("EEXIST", (dest.isDirectory() ? "Folder" : "File") + " '" + path + "' already exists"); - return; - } - try { - boolean result = dest.mkdirs(); - if (!result) { - promise.reject("EUNSPECIFIED", "mkdir failed to create some or all directories in '" + path + "'"); - return; - } - } catch (Exception e) { - promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); - return; - } - promise.resolve(true); - } - - /** - * Copy file to destination path - * @param path Source path - * @param dest Target path - * @param callback JS context callback - */ - static void cp(String path, String dest, Callback callback) { - path = normalizePath(path); - InputStream in = null; - OutputStream out = null; - String message = ""; - - try { - if(!isPathExists(path)) { - callback.invoke("Source file at path`" + path + "` does not exist"); - return; - } - if(!new File(dest).exists()) { - boolean result = new File(dest).createNewFile(); - if (!result) { - callback.invoke("Destination file at '" + dest + "' already exists"); - return; - } - } - - in = inputStreamFromPath(path); - out = new FileOutputStream(dest); - - byte[] buf = new byte[10240]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } catch (Exception err) { - message += err.getLocalizedMessage(); - } finally { - try { - if (in != null) { - in.close(); - } - if (out != null) { - out.close(); - } - } catch (Exception e) { - message += e.getLocalizedMessage(); - } - } - // Only call the callback once to prevent the app from crashing - // with an 'Illegal callback invocation from native module' exception. - if (message != "") { - callback.invoke(message); - } else { - callback.invoke(); - } - } - - /** - * Move file - * @param path Source file path - * @param dest Destination file path - * @param callback JS context callback - */ - static void mv(String path, String dest, Callback callback) { - File src = new File(path); - if(!src.exists()) { - callback.invoke("Source file at path `" + path + "` does not exist"); - return; - } - - try { - InputStream in = new FileInputStream(path); - OutputStream out = new FileOutputStream(dest); - - //read source path to byte buffer. Write from input to output stream - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) { //read is successful - out.write(buffer, 0, read); - } - in.close(); - out.flush(); - - src.delete(); //remove original file - } catch (FileNotFoundException exception) { - callback.invoke("Source file not found."); - return; - } catch (Exception e) { - callback.invoke(e.toString()); - return; - } - - callback.invoke(); - } - - /** - * Check if the path exists, also check if it is a folder when exists. - * @param path Path to check - * @param callback JS context callback - */ - static void exists(String path, Callback callback) { - if(isAsset(path)) { - try { - String filename = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); - com.RNFetchBlob.RNFetchBlob.RCTContext.getAssets().openFd(filename); - callback.invoke(true, false); - } catch (IOException e) { - callback.invoke(false, false); - } - } - else { - path = normalizePath(path); - if (path != null) { - boolean exist = new File(path).exists(); - boolean isDir = new File(path).isDirectory(); - callback.invoke(exist, isDir); - } - else { - callback.invoke(false, false); - } - } - } - - /** - * List content of folder - * @param path Target folder - * @param callback JS context callback - */ - static void ls(String path, Promise promise) { - try { - path = normalizePath(path); - File src = new File(path); - if (!src.exists()) { - promise.reject("ENOENT", "No such file '" + path + "'"); - return; - } - if (!src.isDirectory()) { - promise.reject("ENOTDIR", "Not a directory '" + path + "'"); - return; - } - String[] files = new File(path).list(); - WritableArray arg = Arguments.createArray(); - // File => list(): "If this abstract pathname does not denote a directory, then this method returns null." - // We excluded that possibility above - ignore the "can produce NullPointerException" warning of the IDE. - for (String i : files) { - arg.pushString(i); - } - promise.resolve(arg); - } catch (Exception e) { - e.printStackTrace(); - promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); - } - } - - /** - * Create a file by slicing given file path - * @param path Source file path - * @param dest Destination of created file - * @param start Start byte offset in source file - * @param end End byte offset - * @param encode NOT IMPLEMENTED - */ - static void slice(String path, String dest, int start, int end, String encode, Promise promise) { - try { - path = normalizePath(path); - File source = new File(path); - if(source.isDirectory()){ - promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory"); - return; - } - if(!source.exists()){ - promise.reject("ENOENT", "No such file '" + path + "'"); - return; - } - int size = (int) source.length(); - int max = Math.min(size, end); - int expected = max - start; - int now = 0; - FileInputStream in = new FileInputStream(new File(path)); - FileOutputStream out = new FileOutputStream(new File(dest)); - int skipped = (int) in.skip(start); - if (skipped != start) { - promise.reject("EUNSPECIFIED", "Skipped " + skipped + " instead of the specified " + start + " bytes, size is " + size); - return; - } - byte[] buffer = new byte[10240]; - while(now < expected) { - int read = in.read(buffer, 0, 10240); - int remain = expected - now; - if(read <= 0) { - break; - } - out.write(buffer, 0, (int) Math.min(remain, read)); - now += read; - } - in.close(); - out.flush(); - out.close(); - promise.resolve(dest); - } catch (Exception e) { - e.printStackTrace(); - promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); - } - } - - static void lstat(String path, final Callback callback) { - path = normalizePath(path); - - new AsyncTask() { - @Override - protected Integer doInBackground(String ...args) { - WritableArray res = Arguments.createArray(); - if(args[0] == null) { - callback.invoke("the path specified for lstat is either `null` or `undefined`."); - return 0; - } - File src = new File(args[0]); - if(!src.exists()) { - callback.invoke("failed to lstat path `" + args[0] + "` because it does not exist or it is not a folder"); - return 0; - } - if(src.isDirectory()) { - String [] files = src.list(); - // File => list(): "If this abstract pathname does not denote a directory, then this method returns null." - // We excluded that possibility above - ignore the "can produce NullPointerException" warning of the IDE. - for(String p : files) { - res.pushMap(statFile(src.getPath() + "/" + p)); - } - } - else { - res.pushMap(statFile(src.getAbsolutePath())); - } - callback.invoke(null, res); - return 0; - } - }.execute(path); - } - - /** - * show status of a file or directory - * @param path Path - * @param callback Callback - */ - static void stat(String path, Callback callback) { - try { - path = normalizePath(path); - WritableMap result = statFile(path); - if(result == null) - callback.invoke("failed to stat path `" + path + "` because it does not exist or it is not a folder", null); - else - callback.invoke(null, result); - } catch(Exception err) { - callback.invoke(err.getLocalizedMessage()); - } - } - - /** - * Basic stat method - * @param path Path - * @return Stat Result of a file or path - */ - static WritableMap statFile(String path) { - try { - path = normalizePath(path); - WritableMap stat = Arguments.createMap(); - if(isAsset(path)) { - String name = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); - AssetFileDescriptor fd = RNFetchBlob.RCTContext.getAssets().openFd(name); - stat.putString("filename", name); - stat.putString("path", path); - stat.putString("type", "asset"); - stat.putString("size", String.valueOf(fd.getLength())); - stat.putInt("lastModified", 0); - } - else { - File target = new File(path); - if (!target.exists()) { - return null; - } - stat.putString("filename", target.getName()); - stat.putString("path", target.getPath()); - stat.putString("type", target.isDirectory() ? "directory" : "file"); - stat.putString("size", String.valueOf(target.length())); - String lastModified = String.valueOf(target.lastModified()); - stat.putString("lastModified", lastModified); - - } - return stat; - } catch(Exception err) { - return null; - } - } - - /** - * Media scanner scan file - * @param path Path to file - * @param mimes Array of MIME type strings - * @param callback Callback for results - */ - void scanFile(String [] path, String[] mimes, final Callback callback) { - try { - MediaScannerConnection.scanFile(mCtx, path, mimes, new MediaScannerConnection.OnScanCompletedListener() { - @Override - public void onScanCompleted(String s, Uri uri) { - callback.invoke(null, true); - } - }); - } catch(Exception err) { - callback.invoke(err.getLocalizedMessage(), null); - } - } - - static void hash(String path, String algorithm, Promise promise) { - try { - Map algorithms = new HashMap<>(); - - algorithms.put("md5", "MD5"); - algorithms.put("sha1", "SHA-1"); - algorithms.put("sha224", "SHA-224"); - algorithms.put("sha256", "SHA-256"); - algorithms.put("sha384", "SHA-384"); - algorithms.put("sha512", "SHA-512"); - - if (!algorithms.containsKey(algorithm)) { - promise.reject("EINVAL", "Invalid algorithm '" + algorithm + "', must be one of md5, sha1, sha224, sha256, sha384, sha512"); - return; - } - - File file = new File(path); - - if (file.isDirectory()) { - promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory"); - return; - } - - if (!file.exists()) { - promise.reject("ENOENT", "No such file '" + path + "'"); - return; - } - - MessageDigest md = MessageDigest.getInstance(algorithms.get(algorithm)); - - FileInputStream inputStream = new FileInputStream(path); - int chunkSize = 4096 * 256; // 1Mb - byte[] buffer = new byte[chunkSize]; - - if(file.length() != 0) { - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - md.update(buffer, 0, bytesRead); - } - } - - StringBuilder hexString = new StringBuilder(); - for (byte digestByte : md.digest()) - hexString.append(String.format("%02x", digestByte)); - - promise.resolve(hexString.toString()); - } catch (Exception e) { - e.printStackTrace(); - promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); - } - } - - /** - * Create new file at path - * @param path The destination path of the new file. - * @param data Initial data of the new file. - * @param encoding Encoding of initial data. - * @param promise Promise for Javascript - */ - static void createFile(String path, String data, String encoding, Promise promise) { - try { - File dest = new File(path); - boolean created = dest.createNewFile(); - if(encoding.equals(RNFetchBlobConst.DATA_ENCODE_URI)) { - String orgPath = data.replace(RNFetchBlobConst.FILE_PREFIX, ""); - File src = new File(orgPath); - if(!src.exists()) { - promise.reject("ENOENT", "Source file : " + data + " does not exist"); - return ; - } - FileInputStream fin = new FileInputStream(src); - OutputStream ostream = new FileOutputStream(dest); - byte[] buffer = new byte[10240]; - int read = fin.read(buffer); - while (read > 0) { - ostream.write(buffer, 0, read); - read = fin.read(buffer); - } - fin.close(); - ostream.close(); - } else { - if (!created) { - promise.reject("EEXIST", "File `" + path + "` already exists"); - return; - } - OutputStream ostream = new FileOutputStream(dest); - ostream.write(RNFetchBlobFS.stringToBytes(data, encoding)); - } - promise.resolve(path); - } catch(Exception err) { - promise.reject("EUNSPECIFIED", err.getLocalizedMessage()); - } - } - - /** - * Create file for ASCII encoding - * @param path Path of new file. - * @param data Content of new file - * @param promise JS Promise - */ - static void createFileASCII(String path, ReadableArray data, Promise promise) { - try { - File dest = new File(path); - boolean created = dest.createNewFile(); - if(!created) { - promise.reject("EEXIST", "File at path `" + path + "` already exists"); - return; - } - OutputStream ostream = new FileOutputStream(dest); - byte[] chunk = new byte[data.size()]; - for(int i=0; i= Build.VERSION_CODES.JELLY_BEAN_MR2) { - args.putString("internal_free", String.valueOf(stat.getFreeBytes())); - args.putString("internal_total", String.valueOf(stat.getTotalBytes())); - StatFs statEx = new StatFs(Environment.getExternalStorageDirectory().getPath()); - args.putString("external_free", String.valueOf(statEx.getFreeBytes())); - args.putString("external_total", String.valueOf(statEx.getTotalBytes())); - - } - callback.invoke(null ,args); - } - - /** - * Remove files in session. - * @param paths An array of file paths. - * @param callback JS contest callback - */ - static void removeSession(ReadableArray paths, final Callback callback) { - AsyncTask task = new AsyncTask() { - @Override - protected Integer doInBackground(ReadableArray ...paths) { - try { - ArrayList failuresToDelete = new ArrayList<>(); - for (int i = 0; i < paths[0].size(); i++) { - String fileName = paths[0].getString(i); - File f = new File(fileName); - if (f.exists()) { - boolean result = f.delete(); - if (!result) { - failuresToDelete.add(fileName); - } - } - } - if (failuresToDelete.isEmpty()) { - callback.invoke(null, true); - } else { - StringBuilder listString = new StringBuilder(); - listString.append("Failed to delete: "); - for (String s : failuresToDelete) { - listString.append(s).append(", "); - } - callback.invoke(listString.toString()); - } - } catch(Exception err) { - callback.invoke(err.getLocalizedMessage()); - } - return paths[0].size(); - } - }; - task.execute(paths); - } - - /** - * String to byte converter method - * @param data Raw data in string format - * @param encoding Decoder name - * @return Converted data byte array - */ - private static byte[] stringToBytes(String data, String encoding) { - if(encoding.equalsIgnoreCase("ascii")) { - return data.getBytes(Charset.forName("US-ASCII")); - } - else if(encoding.toLowerCase().contains("base64")) { - return Base64.decode(data, Base64.NO_WRAP); - - } - else if(encoding.equalsIgnoreCase("utf8")) { - return data.getBytes(Charset.forName("UTF-8")); - } - return data.getBytes(Charset.forName("US-ASCII")); - } - - /** - * Private method for emit read stream event. - * @param streamName ID of the read stream - * @param event Event name, `data`, `end`, `error`, etc. - * @param data Event data - */ - private void emitStreamEvent(String streamName, String event, String data) { - WritableMap eventData = Arguments.createMap(); - eventData.putString("event", event); - eventData.putString("detail", data); - this.emitter.emit(streamName, eventData); - } - - // "event" always is "data"... - private void emitStreamEvent(String streamName, String event, WritableArray data) { - WritableMap eventData = Arguments.createMap(); - eventData.putString("event", event); - eventData.putArray("detail", data); - this.emitter.emit(streamName, eventData); - } - - // "event" always is "error"... - private void emitStreamEvent(String streamName, String event, String code, String message) { - WritableMap eventData = Arguments.createMap(); - eventData.putString("event", event); - eventData.putString("code", code); - eventData.putString("detail", message); - this.emitter.emit(streamName, eventData); - } - - /** - * Get input stream of the given path, when the path is a string starts with bundle-assets:// - * the stream is created by Assets Manager, otherwise use FileInputStream. - * @param path The file to open stream - * @return InputStream instance - * @throws IOException If the given file does not exist or is a directory FileInputStream will throw a FileNotFoundException - */ - private static InputStream inputStreamFromPath(String path) throws IOException { - if (path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { - return RNFetchBlob.RCTContext.getAssets().open(path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "")); - } - return new FileInputStream(new File(path)); - } - - /** - * Check if the asset or the file exists - * @param path A file path URI string - * @return A boolean value represents if the path exists. - */ - private static boolean isPathExists(String path) { - if(path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { - try { - RNFetchBlob.RCTContext.getAssets().open(path.replace(com.RNFetchBlob.RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "")); - } catch (IOException e) { - return false; - } - return true; - } - else { - return new File(path).exists(); - } - - } - - static boolean isAsset(String path) { - return path != null && path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET); - } - - /** - * Normalize the path, remove URI scheme (xxx://) so that we can handle it. - * @param path URI string. - * @return Normalized string - */ - static String normalizePath(String path) { - if(path == null) - return null; - if(!path.matches("\\w+\\:.*")) - return path; - if(path.startsWith("file://")) { - return path.replace("file://", ""); - } - - Uri uri = Uri.parse(path); - if(path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { - return path; - } - else - return PathResolver.getRealPathFromURI(RNFetchBlob.RCTContext, uri); - } - -} +package com.ReactNativeBlobUtil; + +import android.content.res.AssetFileDescriptor; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.util.Base64; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.events.EventDispatcher; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +class ReactNativeBlobUtilFS { + + private ReactApplicationContext mCtx; + private DeviceEventManagerModule.RCTDeviceEventEmitter emitter; + + ReactNativeBlobUtilFS(ReactApplicationContext ctx) { + this.mCtx = ctx; + this.emitter = ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + } + + /** + * Write string with encoding to file (used for mediastore) + * + * @param path Destination file path. + * @param encoding Encoding of the string. + * @param data Array passed from JS context. + */ + static boolean writeFile(String path, String encoding, String data, final boolean append) { + try { + int written; + path = ReactNativeBlobUtilUtils.normalizePath(path); + File f = new File(path); + File dir = f.getParentFile(); + if (!f.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs() && !dir.exists()) { + return false; + } + } + if (!f.createNewFile()) { + return false; + } + } + + // write data from a file + if (encoding.equalsIgnoreCase(ReactNativeBlobUtilConst.DATA_ENCODE_URI)) { + String normalizedData = ReactNativeBlobUtilUtils.normalizePath(data); + File src = new File(normalizedData); + if (!src.exists()) { + return false; + } + byte[] buffer = new byte[10240]; + int read; + written = 0; + FileInputStream fin = null; + FileOutputStream fout = null; + try { + fin = new FileInputStream(src); + fout = new FileOutputStream(f, append); + while ((read = fin.read(buffer)) > 0) { + fout.write(buffer, 0, read); + written += read; + } + } finally { + if (fin != null) { + fin.close(); + } + if (fout != null) { + fout.close(); + } + } + } else { + byte[] bytes = ReactNativeBlobUtilUtils.stringToBytes(data, encoding); + FileOutputStream fout = new FileOutputStream(f, append); + try { + fout.write(bytes); + written = bytes.length; + } finally { + fout.close(); + } + } + return true; + } catch (FileNotFoundException e) { + // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html + return false; + } catch (Exception e) { + return false; + } + } + + /** + * Write string with encoding to file + * + * @param path Destination file path. + * @param encoding Encoding of the string. + * @param data Array passed from JS context. + * @param promise RCT Promise + */ + static void writeFile(String path, String encoding, String data, final boolean transformFile, final boolean append, final Promise promise) { + try { + int written; + File f = new File(path); + File dir = f.getParentFile(); + if (!f.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs() && !dir.exists()) { + promise.reject("EUNSPECIFIED", "Failed to create parent directory of '" + path + "'"); + return; + } + } + if (!f.createNewFile()) { + promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created"); + return; + } + } + + // write data from a file + if (encoding.equalsIgnoreCase(ReactNativeBlobUtilConst.DATA_ENCODE_URI)) { + String normalizedData = ReactNativeBlobUtilUtils.normalizePath(data); + File src = new File(normalizedData); + if (!src.exists()) { + promise.reject("ENOENT", "No such file '" + path + "' " + "('" + normalizedData + "')"); + return; + } + byte[] buffer = new byte[10240]; + int read; + written = 0; + FileInputStream fin = null; + FileOutputStream fout = null; + try { + fin = new FileInputStream(src); + fout = new FileOutputStream(f, append); + while ((read = fin.read(buffer)) > 0) { + fout.write(buffer, 0, read); + written += read; + } + } finally { + if (fin != null) { + fin.close(); + } + if (fout != null) { + fout.close(); + } + } + } else { + byte[] bytes = ReactNativeBlobUtilUtils.stringToBytes(data, encoding); + if (transformFile) { + if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) { + throw new IllegalStateException("Write file with transform was specified but the shared file transformer is not set"); + } + bytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(bytes); + } + FileOutputStream fout = new FileOutputStream(f, append); + try { + fout.write(bytes); + written = bytes.length; + } finally { + fout.close(); + } + } + promise.resolve(written); + } catch (FileNotFoundException e) { + // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html + promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created, or it is a directory"); + } catch (Exception e) { + promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); + } + } + + /** + * Write array of bytes into file + * + * @param path Destination file path. + * @param data Array passed from JS context. + * @param promise RCT Promise + */ + static void writeFile(String path, ReadableArray data, final boolean append, final Promise promise) { + try { + File f = new File(path); + File dir = f.getParentFile(); + + if (!f.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs() && !dir.exists()) { + promise.reject("ENOTDIR", "Failed to create parent directory of '" + path + "'"); + return; + } + } + if (!f.createNewFile()) { + promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created"); + return; + } + } + + FileOutputStream os = new FileOutputStream(f, append); + try { + byte[] bytes = new byte[data.size()]; + for (int i = 0; i < data.size(); i++) { + bytes[i] = (byte) data.getInt(i); + } + os.write(bytes); + } finally { + os.close(); + } + promise.resolve(data.size()); + } catch (FileNotFoundException e) { + // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html + promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created"); + } catch (Exception e) { + promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); + } + } + + /** + * Read file with a buffer that has the same size as the target file. + * + * @param path Path of the file. + * @param encoding Encoding of read stream. + * @param promise JS promise + */ + static void readFile(String path, String encoding, final boolean transformFile, final Promise promise) { + String resolved = ReactNativeBlobUtilUtils.normalizePath(path); + if (resolved != null) + path = resolved; + try { + byte[] bytes; + int bytesRead; + int length; // max. array length limited to "int", also see https://stackoverflow.com/a/10787175/544779 + + if (resolved != null && resolved.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET)) { + String assetName = path.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, ""); + // This fails should an asset file be >2GB + InputStream in = ReactNativeBlobUtilImpl.RCTContext.getAssets().open(assetName); + length = in.available(); + bytes = new byte[length]; + bytesRead = in.read(bytes, 0, length); + in.close(); + } + // issue 287 + else if (resolved == null) { + InputStream in = ReactNativeBlobUtilImpl.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); + // TODO See https://developer.android.com/reference/java/io/InputStream.html#available() + // Quote: "Note that while some implementations of InputStream will return the total number of bytes + // in the stream, many will not. It is never correct to use the return value of this method to + // allocate a buffer intended to hold all data in this stream." + length = in.available(); + bytes = new byte[length]; + bytesRead = in.read(bytes); + in.close(); + } else { + File f = new File(path); + length = (int) f.length(); + bytes = new byte[length]; + FileInputStream in = new FileInputStream(f); + bytesRead = in.read(bytes); + in.close(); + } + + if (bytesRead < length) { + promise.reject("EUNSPECIFIED", "Read only " + bytesRead + " bytes of " + length); + return; + } + + if (transformFile) { + if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) { + throw new IllegalStateException("Read file with transform was specified but the shared file transformer is not set"); + } + bytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onReadFile(bytes); + } + + switch (encoding.toLowerCase(Locale.ROOT)) { + case "base64": + promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP)); + break; + case "ascii": + WritableArray asciiResult = Arguments.createArray(); + for (byte b : bytes) { + asciiResult.pushInt((int) b); + } + promise.resolve(asciiResult); + break; + case "utf8": + promise.resolve(new String(bytes)); + break; + default: + promise.resolve(new String(bytes)); + break; + } + } catch (FileNotFoundException err) { + String msg = err.getLocalizedMessage(); + if (msg.contains("EISDIR")) { + promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory; " + msg); + } else { + promise.reject("ENOENT", "No such file '" + path + "'; " + msg); + } + } catch (Exception err) { + promise.reject("EUNSPECIFIED", err.getLocalizedMessage()); + } + + } + + /** + * Static method that returns system folders to JS context + * + * @param ctx React Native application context + */ + static Map getSystemfolders(ReactApplicationContext ctx) { + Map res = new HashMap<>(); + + res.put("DocumentDir", getFilesDirPath(ctx)); + res.put("CacheDir", getCacheDirPath(ctx)); + res.put("DCIMDir", getExternalFilesDirPath(ctx, Environment.DIRECTORY_DCIM)); + res.put("PictureDir", getExternalFilesDirPath(ctx, Environment.DIRECTORY_PICTURES)); + res.put("MusicDir", getExternalFilesDirPath(ctx, Environment.DIRECTORY_MUSIC)); + res.put("DownloadDir", getExternalFilesDirPath(ctx, Environment.DIRECTORY_DOWNLOADS)); + res.put("MovieDir", getExternalFilesDirPath(ctx, Environment.DIRECTORY_MOVIES)); + res.put("RingtoneDir", getExternalFilesDirPath(ctx, Environment.DIRECTORY_RINGTONES)); + + String state = Environment.getExternalStorageState(); + if (state.equals(Environment.MEDIA_MOUNTED)) { + res.put("SDCardDir", getExternalFilesDirPath(ctx, null)); + + File externalDirectory = ctx.getExternalFilesDir(null); + + if (externalDirectory != null && externalDirectory.getParentFile() != null) { + res.put("SDCardApplicationDir", externalDirectory.getParentFile().getAbsolutePath()); + } else { + res.put("SDCardApplicationDir", ""); + } + } else { + res.put("SDCardDir", ""); + res.put("SDCardApplicationDir", ""); + } + + res.put("MainBundleDir", ctx.getApplicationInfo().dataDir); + + // TODO Change me with the correct path + res.put("LibraryDir", ""); + res.put("ApplicationSupportDir", ""); + + return res; + } + + /** + * Static method that returns legacy system folders to JS context (usage of deprecated functions since these retunr different folders) + * + * @param ctx React Native application context + */ + + @NonNull + @SuppressWarnings("deprecation") + static Map getLegacySystemfolders(ReactApplicationContext ctx) { + Map res = new HashMap<>(); + + res.put("LegacyDCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()); + res.put("LegacyPictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + res.put("LegacyMusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath()); + res.put("LegacyDownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()); + res.put("LegacyMovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath()); + res.put("LegacyRingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath()); + + String state = Environment.getExternalStorageState(); + if (state.equals(Environment.MEDIA_MOUNTED)) { + res.put("LegacySDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath()); + } else { + res.put("LegacySDCardDir", ""); + } + + return res; + } + + static String getExternalFilesDirPath(ReactApplicationContext ctx, String type) { + File dir = ctx.getExternalFilesDir(type); + if (dir != null) return dir.getAbsolutePath(); + return ""; + } + + static String getFilesDirPath(ReactApplicationContext ctx) { + File dir = ctx.getFilesDir(); + if (dir != null) return dir.getAbsolutePath(); + return ""; + } + + static String getCacheDirPath(ReactApplicationContext ctx) { + File dir = ctx.getCacheDir(); + if (dir != null) return dir.getAbsolutePath(); + return ""; + } + + static public void getSDCardDir(ReactApplicationContext ctx, Promise promise) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + try { + final String path = ctx.getExternalFilesDir(null).getAbsolutePath(); + promise.resolve(path); + } catch (Exception e) { + promise.reject("ReactNativeBlobUtil.getSDCardDir", e.getLocalizedMessage()); + } + } else { + promise.reject("ReactNativeBlobUtil.getSDCardDir", "External storage not mounted"); + } + + } + + static public void getSDCardApplicationDir(ReactApplicationContext ctx, Promise promise) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + try { + final String path = ctx.getExternalFilesDir(null).getParentFile().getAbsolutePath(); + promise.resolve(path); + } catch (Exception e) { + promise.reject("ReactNativeBlobUtil.getSDCardApplicationDir", e.getLocalizedMessage()); + } + } else { + promise.reject("ReactNativeBlobUtil.getSDCardApplicationDir", "External storage not mounted"); + } + } + + /** + * Static method that returns a temp file path + * + * @param taskId An unique string for identify + * @return String + */ + static String getTmpPath(String taskId) { + return ReactNativeBlobUtilImpl.RCTContext.getFilesDir() + "/ReactNativeBlobUtilTmp_" + taskId; + } + + + /** + * Unlink file at path + * + * @param path Path of target + * @param callback JS context callback + */ + static void unlink(String path, Callback callback) { + try { + String normalizedPath = ReactNativeBlobUtilUtils.normalizePath(path); + ReactNativeBlobUtilFS.deleteRecursive(new File(normalizedPath)); + callback.invoke(null, true); + } catch (Exception err) { + callback.invoke(err.getLocalizedMessage(), false); + } + } + + private static void deleteRecursive(File fileOrDirectory) throws IOException { + if (fileOrDirectory.isDirectory()) { + File[] files = fileOrDirectory.listFiles(); + if (files == null) { + throw new NullPointerException("Received null trying to list files of directory '" + fileOrDirectory + "'"); + } else { + for (File child : files) { + deleteRecursive(child); + } + } + } + boolean result = fileOrDirectory.delete(); + if (!result) { + throw new IOException("Failed to delete '" + fileOrDirectory + "'"); + } + } + + /** + * Make a folder + * + * @param path Source path + * @param promise JS promise + */ + static void mkdir(String path, Promise promise) { + path = ReactNativeBlobUtilUtils.normalizePath(path); + File dest = new File(path); + if (dest.exists()) { + promise.reject("EEXIST", (dest.isDirectory() ? "Folder" : "File") + " '" + path + "' already exists"); + return; + } + try { + boolean result = dest.mkdirs(); + if (!result) { + promise.reject("EUNSPECIFIED", "mkdir failed to create some or all directories in '" + path + "'"); + return; + } + } catch (Exception e) { + promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); + return; + } + promise.resolve(true); + } + + /** + * Copy file to destination path + * + * @param path Source path + * @param dest Target path + * @param callback JS context callback + */ + static void cp(String path, String dest, Callback callback) { + dest = ReactNativeBlobUtilUtils.normalizePath(dest); + InputStream in = null; + OutputStream out = null; + String message = ""; + + try { + in = inputStreamFromPath(path); + if (in == null) { + callback.invoke("Source file at path`" + path + "` does not exist or can not be opened"); + return; + } + if (!new File(dest).exists()) { + boolean result = new File(dest).createNewFile(); + if (!result) { + callback.invoke("Destination file at '" + dest + "' already exists"); + return; + } + } + + out = new FileOutputStream(dest); + + byte[] buf = new byte[10240]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } catch (Exception err) { + message += err.getLocalizedMessage(); + } finally { + try { + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } catch (Exception e) { + message += e.getLocalizedMessage(); + } + } + // Only call the callback once to prevent the app from crashing + // with an 'Illegal callback invocation from native module' exception. + if (message != "") { + callback.invoke(message); + } else { + callback.invoke(); + } + } + + /** + * Move file + * + * @param path Source file path + * @param dest Destination file path + * @param callback JS context callback + */ + static void mv(String path, String dest, Callback callback) { + path = ReactNativeBlobUtilUtils.normalizePath(path); + dest = ReactNativeBlobUtilUtils.normalizePath(dest); + File src = new File(path); + if (!src.exists()) { + callback.invoke("Source file at path `" + path + "` does not exist"); + return; + } + + try { + // mv should fail if the destination directory does not exist. + File destFile = new File(dest); + File parentDir = destFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + callback.invoke("mv failed because the destination directory doesn't exist"); + return; + } + // mv overwrites files, so delete any existing file. + if (destFile.exists()) { + destFile.delete(); + } + // mv by renaming the file. + boolean result = src.renameTo(destFile); + if (!result) { + callback.invoke("mv failed for unknown reasons"); + return; + } + } catch (Exception e) { + callback.invoke(e.toString()); + return; + } + + callback.invoke(); + } + + /** + * Check if the path exists, also check if it is a folder when exists. + * + * @param path Path to check + * @param callback JS context callback + */ + static void exists(String path, Callback callback) { + if (isAsset(path)) { + try { + String filename = path.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, ""); + com.ReactNativeBlobUtil.ReactNativeBlobUtilImpl.RCTContext.getAssets().openFd(filename); + callback.invoke(true, false); + } catch (IOException e) { + callback.invoke(false, false); + } + } else { + path = ReactNativeBlobUtilUtils.normalizePath(path); + if (path != null) { + boolean exist = new File(path).exists(); + boolean isDir = new File(path).isDirectory(); + callback.invoke(exist, isDir); + } else { + callback.invoke(false, false); + } + } + } + + /** + * List content of folder + * + * @param path Target folder + * @param promise JS context promise + */ + static void ls(String path, Promise promise) { + try { + path = ReactNativeBlobUtilUtils.normalizePath(path); + File src = new File(path); + if (!src.exists()) { + promise.reject("ENOENT", "No such file '" + path + "'"); + return; + } + if (!src.isDirectory()) { + promise.reject("ENOTDIR", "Not a directory '" + path + "'"); + return; + } + String[] files = new File(path).list(); + WritableArray arg = Arguments.createArray(); + // File => list(): "If this abstract pathname does not denote a directory, then this method returns null." + // We excluded that possibility above - ignore the "can produce NullPointerException" warning of the IDE. + for (String i : files) { + arg.pushString(i); + } + promise.resolve(arg); + } catch (Exception e) { + e.printStackTrace(); + promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); + } + } + + /** + * Create a file by slicing given file path + * + * @param path Source file path + * @param dest Destination of created file + * @param start Start byte offset in source file + * @param end End byte offset + * @param encode NOT IMPLEMENTED + */ + static void slice(String path, String dest, long start, long end, String encode, Promise promise) { + try { + dest = ReactNativeBlobUtilUtils.normalizePath(dest); + + if (!path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_CONTENT)) { + File file = new File(ReactNativeBlobUtilUtils.normalizePath(path)); + if (file.isDirectory()) { + promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory"); + return; + } + } + + InputStream in = inputStreamFromPath(path); + if (in == null) { + promise.reject("ENOENT", "No such file '" + path + "'"); + return; + } + FileOutputStream out = new FileOutputStream(new File(dest)); + long skipped = in.skip(start); + if (skipped != start) { + promise.reject("EUNSPECIFIED", "Skipped " + skipped + " instead of the specified " + start + " bytes"); + return; + } + byte[] buffer = new byte[10240]; + int remain = (int) (end - start); + while (remain > 0) { + int read = in.read(buffer, 0, 10240); + if (read <= 0) { + break; + } + out.write(buffer, 0, (int) Math.min(remain, read)); + remain -= read; + } + in.close(); + out.flush(); + out.close(); + promise.resolve(dest); + } catch (Exception e) { + e.printStackTrace(); + promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); + } + } + + static void lstat(String path, final Callback callback) { + path = ReactNativeBlobUtilUtils.normalizePath(path); + + new AsyncTask() { + @Override + protected Integer doInBackground(String... args) { + WritableArray res = Arguments.createArray(); + if (args[0] == null) { + callback.invoke("the path specified for lstat is either `null` or `undefined`."); + return 0; + } + File src = new File(args[0]); + if (!src.exists()) { + callback.invoke("failed to lstat path `" + args[0] + "` because it does not exist or it is not a folder"); + return 0; + } + if (src.isDirectory()) { + String[] files = src.list(); + // File => list(): "If this abstract pathname does not denote a directory, then this method returns null." + // We excluded that possibility above - ignore the "can produce NullPointerException" warning of the IDE. + for (String p : files) { + res.pushMap(statFile(src.getPath() + "/" + p)); + } + } else { + res.pushMap(statFile(src.getAbsolutePath())); + } + callback.invoke(null, res); + return 0; + } + }.execute(path); + } + + /** + * show status of a file or directory + * + * @param path Path + * @param callback Callback + */ + static void stat(String path, Callback callback) { + try { + path = ReactNativeBlobUtilUtils.normalizePath(path); + WritableMap result = statFile(path); + if (result == null) + callback.invoke("failed to stat path `" + path + "` because it does not exist or it is not a folder", null); + else + callback.invoke(null, result); + } catch (Exception err) { + callback.invoke(err.getLocalizedMessage()); + } + } + + /** + * Basic stat method + * + * @param path Path + * @return Stat Result of a file or path + */ + static WritableMap statFile(String path) { + try { + path = ReactNativeBlobUtilUtils.normalizePath(path); + WritableMap stat = Arguments.createMap(); + if (isAsset(path)) { + String name = path.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, ""); + AssetFileDescriptor fd = ReactNativeBlobUtilImpl.RCTContext.getAssets().openFd(name); + stat.putString("filename", name); + stat.putString("path", path); + stat.putString("type", "asset"); + stat.putString("size", String.valueOf(fd.getLength())); + stat.putInt("lastModified", 0); + } else { + File target = new File(path); + if (!target.exists()) { + return null; + } + stat.putString("filename", target.getName()); + stat.putString("path", target.getPath()); + stat.putString("type", target.isDirectory() ? "directory" : "file"); + stat.putString("size", String.valueOf(target.length())); + String lastModified = String.valueOf(target.lastModified()); + stat.putString("lastModified", lastModified); + + } + return stat; + } catch (Exception err) { + return null; + } + } + + /** + * Media scanner scan file + * + * @param path Path to file + * @param mimes Array of MIME type strings + * @param callback Callback for results + */ + void scanFile(String[] path, String[] mimes, final Callback callback) { + try { + MediaScannerConnection.scanFile(mCtx, path, mimes, new MediaScannerConnection.OnScanCompletedListener() { + @Override + public void onScanCompleted(String s, Uri uri) { + callback.invoke(null, true); + } + }); + } catch (Exception err) { + callback.invoke(err.getLocalizedMessage(), null); + } + } + + static void hash(String path, String algorithm, Promise promise) { + try { + Map algorithms = new HashMap<>(); + + algorithms.put("md5", "MD5"); + algorithms.put("sha1", "SHA-1"); + algorithms.put("sha224", "SHA-224"); + algorithms.put("sha256", "SHA-256"); + algorithms.put("sha384", "SHA-384"); + algorithms.put("sha512", "SHA-512"); + + if (!algorithms.containsKey(algorithm)) { + promise.reject("EINVAL", "Invalid algorithm '" + algorithm + "', must be one of md5, sha1, sha224, sha256, sha384, sha512"); + return; + } + + if (!path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_CONTENT)) { + File file = new File(ReactNativeBlobUtilUtils.normalizePath(path)); + if (file.isDirectory()) { + promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory"); + return; + } + } + + MessageDigest md = MessageDigest.getInstance(algorithms.get(algorithm)); + + InputStream inputStream = inputStreamFromPath(path); + if (inputStream == null) { + promise.reject("ENOENT", "No such file '" + path + "'"); + return; + } + int chunkSize = 4096 * 256; // 1Mb + byte[] buffer = new byte[chunkSize]; + + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead); + } + + StringBuilder hexString = new StringBuilder(); + for (byte digestByte : md.digest()) + hexString.append(String.format("%02x", digestByte)); + + promise.resolve(hexString.toString()); + } catch (Exception e) { + e.printStackTrace(); + promise.reject("EUNSPECIFIED", e.getLocalizedMessage()); + } + } + + /** + * Create new file at path + * + * @param path The destination path of the new file. + * @param data Initial data of the new file. + * @param encoding Encoding of initial data. + * @param promise Promise for Javascript + */ + static void createFile(String path, String data, String encoding, Promise promise) { + try { + path = ReactNativeBlobUtilUtils.normalizePath(path); + File dest = new File(path); + boolean created = dest.createNewFile(); + if (encoding.equals(ReactNativeBlobUtilConst.DATA_ENCODE_URI)) { + String orgPath = data.replace(ReactNativeBlobUtilConst.FILE_PREFIX, ""); + File src = new File(orgPath); + if (!src.exists()) { + promise.reject("ENOENT", "Source file : " + data + " does not exist"); + return; + } + FileInputStream fin = new FileInputStream(src); + OutputStream ostream = new FileOutputStream(dest); + byte[] buffer = new byte[10240]; + int read = fin.read(buffer); + while (read > 0) { + ostream.write(buffer, 0, read); + read = fin.read(buffer); + } + fin.close(); + ostream.close(); + } else { + if (!created) { + promise.reject("EEXIST", "File `" + path + "` already exists"); + return; + } + OutputStream ostream = new FileOutputStream(dest); + ostream.write(ReactNativeBlobUtilUtils.stringToBytes(data, encoding)); + } + promise.resolve(path); + } catch (Exception err) { + promise.reject("EUNSPECIFIED", err.getLocalizedMessage()); + } + } + + /** + * Create file for ASCII encoding + * + * @param path Path of new file. + * @param data Content of new file + * @param promise JS Promise + */ + static void createFileASCII(String path, ReadableArray data, Promise promise) { + try { + path = ReactNativeBlobUtilUtils.normalizePath(path); + File dest = new File(path); + boolean created = dest.createNewFile(); + if (!created) { + promise.reject("EEXIST", "File at path `" + path + "` already exists"); + return; + } + OutputStream ostream = new FileOutputStream(dest); + byte[] chunk = new byte[data.size()]; + for (int i = 0; i < data.size(); i++) { + chunk[i] = (byte) data.getInt(i); + } + ostream.write(chunk); + promise.resolve(path); + } catch (Exception err) { + promise.reject("EUNSPECIFIED", err.getLocalizedMessage()); + } + } + + static void df(Callback callback, ReactApplicationContext ctx) { + StatFs stat = new StatFs(ctx.getFilesDir().getPath()); + WritableMap args = Arguments.createMap(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + args.putString("internal_free", String.valueOf(stat.getFreeBytes())); + args.putString("internal_total", String.valueOf(stat.getTotalBytes())); + File dir = ctx.getExternalFilesDir(null); + if (dir != null) { + StatFs statEx = new StatFs(dir.getPath()); + args.putString("external_free", String.valueOf(statEx.getFreeBytes())); + args.putString("external_total", String.valueOf(statEx.getTotalBytes())); + } else { + args.putString("external_free", "-1"); + args.putString("external_total", "-1"); + } + } + callback.invoke(null, args); + } + + /** + * Remove files in session. + * + * @param paths An array of file paths. + * @param callback JS contest callback + */ + static void removeSession(ReadableArray paths, final Callback callback) { + AsyncTask task = new AsyncTask() { + @Override + protected Integer doInBackground(ReadableArray... paths) { + try { + ArrayList failuresToDelete = new ArrayList<>(); + for (int i = 0; i < paths[0].size(); i++) { + String fileName = paths[0].getString(i); + File f = new File(fileName); + if (f.exists()) { + boolean result = f.delete(); + if (!result) { + failuresToDelete.add(fileName); + } + } + } + if (failuresToDelete.isEmpty()) { + callback.invoke(null, true); + } else { + StringBuilder listString = new StringBuilder(); + listString.append("Failed to delete: "); + for (String s : failuresToDelete) { + listString.append(s).append(", "); + } + callback.invoke(listString.toString()); + } + } catch (Exception err) { + callback.invoke(err.getLocalizedMessage()); + } + return paths[0].size(); + } + }; + task.execute(paths); + } + + /** + * Get input stream of the given path. + * When the path starts with bundle-assets:// the stream is created by Assets Manager + * When the path starts with content:// the stream is created by ContentResolver + * otherwise use FileInputStream. + * + * @param path The file to open stream + * @return InputStream instance + * @throws IOException If the given file does not exist or is a directory FileInputStream will throw a FileNotFoundException + */ + private static InputStream inputStreamFromPath(String path) throws IOException { + if (path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET)) { + return ReactNativeBlobUtilImpl.RCTContext.getAssets().open(path.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, "")); + } + if (path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_CONTENT)) { + return ReactNativeBlobUtilImpl.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); + } + return new FileInputStream(new File(ReactNativeBlobUtilUtils.normalizePath(path))); + } + + /** + * Check if the asset or the file exists + * + * @param path A file path URI string + * @return A boolean value represents if the path exists. + */ + private static boolean isPathExists(String path) { + if (path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET)) { + try { + ReactNativeBlobUtilImpl.RCTContext.getAssets().open(path.replace(com.ReactNativeBlobUtil.ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, "")); + } catch (IOException e) { + return false; + } + return true; + } else { + return new File(path).exists(); + } + + } + + static boolean isAsset(String path) { + return path != null && path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET); + } + +} diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFileTransformer.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFileTransformer.java new file mode 100644 index 000000000..171de38d5 --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFileTransformer.java @@ -0,0 +1,10 @@ +package com.ReactNativeBlobUtil; + +public class ReactNativeBlobUtilFileTransformer { + public interface FileTransformer { + public byte[] onWriteFile(byte[] data); + public byte[] onReadFile(byte[] data); + } + + public static ReactNativeBlobUtilFileTransformer.FileTransformer sharedFileTransformer; +} \ No newline at end of file diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilImpl.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilImpl.java new file mode 100644 index 000000000..e629bbadf --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilImpl.java @@ -0,0 +1,431 @@ +package com.ReactNativeBlobUtil; + +import android.app.Activity; +import android.app.DownloadManager; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.util.SparseArray; + +import androidx.annotation.RequiresApi; +import androidx.core.content.FileProvider; + +import com.ReactNativeBlobUtil.Utils.FileDescription; +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.network.CookieJarContainer; +import com.facebook.react.modules.network.ForwardingCookieHandler; +import com.facebook.react.modules.network.OkHttpClientProvider; + +import java.io.File; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; + +import static android.app.Activity.RESULT_OK; +import static com.ReactNativeBlobUtil.ReactNativeBlobUtilConst.GET_CONTENT_INTENT; + +class ReactNativeBlobUtilImpl { + + public static final String NAME = "ReactNativeBlobUtil"; + + private final OkHttpClient mClient; + + static ReactApplicationContext RCTContext; + private static final LinkedBlockingQueue taskQueue = new LinkedBlockingQueue<>(); + private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 5000, TimeUnit.MILLISECONDS, taskQueue); + static LinkedBlockingQueue fsTaskQueue = new LinkedBlockingQueue<>(); + private static final ThreadPoolExecutor fsThreadPool = new ThreadPoolExecutor(2, 10, 5000, TimeUnit.MILLISECONDS, taskQueue); + private static boolean ActionViewVisible = false; + private static final SparseArray promiseTable = new SparseArray<>(); + + public ReactNativeBlobUtilImpl(ReactApplicationContext reactContext) { + mClient = OkHttpClientProvider.getOkHttpClient(); + ForwardingCookieHandler mCookieHandler = new ForwardingCookieHandler(reactContext); + CookieJarContainer mCookieJarContainer = (CookieJarContainer) mClient.cookieJar(); + mCookieJarContainer.setCookieJar(new JavaNetCookieJar(mCookieHandler)); + + RCTContext = reactContext; + reactContext.addActivityEventListener(new ActivityEventListener() { + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (requestCode == GET_CONTENT_INTENT && resultCode == RESULT_OK) { + Uri d = data.getData(); + promiseTable.get(GET_CONTENT_INTENT).resolve(d.toString()); + promiseTable.remove(GET_CONTENT_INTENT); + } + } + + @Override + public void onNewIntent(Intent intent) { + + } + }); + } + + public void createFile(final String path, final String content, final String encode, final Promise promise) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.createFile(path, content, encode, promise); + } + }); + } + + public void createFileASCII(final String path, final ReadableArray dataArray, final Promise promise) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.createFileASCII(path, dataArray, promise); + } + }); + } + + public void actionViewIntent(String path, String mime, @Nullable String chooserTitle, final Promise promise) { + try { + Uri uriForFile = null; + if (!ReactNativeBlobUtilUtils.isContentUri(path)) { + uriForFile = FileProvider.getUriForFile(RCTContext, + RCTContext.getPackageName() + ".provider", new File(path)); + } else { + uriForFile = Uri.parse(path); + } + Intent intent = new Intent(Intent.ACTION_VIEW); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // Create the intent with data and type + intent.setDataAndType(uriForFile, mime); + + // Set flag to give temporary permission to external app to use FileProvider + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // All the activity to be opened outside of an activity + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } else { + intent.setDataAndType(Uri.parse("file://" + path), mime).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + if (chooserTitle != null) { + intent = Intent.createChooser(intent, chooserTitle); + } + + try { + RCTContext.startActivity(intent); + promise.resolve(true); + } catch (ActivityNotFoundException ex) { + promise.reject("ENOAPP", "No app installed for " + mime); + } + + ActionViewVisible = true; + + final LifecycleEventListener listener = new LifecycleEventListener() { + + @Override + public void onHostResume() { + if (ActionViewVisible) + promise.resolve(null); + RCTContext.removeLifecycleEventListener(this); + } + + @Override + public void onHostPause() { + + } + + @Override + public void onHostDestroy() { + + } + }; + RCTContext.addLifecycleEventListener(listener); + } catch (Exception ex) { + promise.reject("EUNSPECIFIED", ex.getLocalizedMessage()); + } + } + + public void writeArrayChunk(final String streamId, final ReadableArray dataArray, final Callback callback) { + ReactNativeBlobUtilStream.writeArrayChunk(streamId, dataArray, callback); + } + + public void unlink(String path, Callback callback) { + ReactNativeBlobUtilFS.unlink(path, callback); + } + + public void mkdir(String path, Promise promise) { + ReactNativeBlobUtilFS.mkdir(path, promise); + } + + public void exists(String path, Callback callback) { + ReactNativeBlobUtilFS.exists(path, callback); + } + + public void cp(final String path, final String dest, final Callback callback) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.cp(path, dest, callback); + } + }); + } + + public void mv(String path, String dest, Callback callback) { + ReactNativeBlobUtilFS.mv(path, dest, callback); + } + + public void ls(String path, Promise promise) { + ReactNativeBlobUtilFS.ls(path, promise); + } + + public void writeStream(String path, String encode, boolean append, Callback callback) { + new ReactNativeBlobUtilStream(RCTContext).writeStream(path, encode, append, callback); + } + + public void writeChunk(String streamId, String data, Callback callback) { + ReactNativeBlobUtilStream.writeChunk(streamId, data, callback); + } + + public void closeStream(String streamId, Callback callback) { + ReactNativeBlobUtilStream.closeStream(streamId, callback); + } + + public void removeSession(ReadableArray paths, Callback callback) { + ReactNativeBlobUtilFS.removeSession(paths, callback); + } + + public void readFile(final String path, final String encoding, final boolean transformFile, final Promise promise) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.readFile(path, encoding, transformFile, promise); + } + }); + } + + public void writeFileArray(final String path, final ReadableArray data, final boolean append, final Promise promise) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.writeFile(path, data, append, promise); + } + }); + } + + public void writeFile(final String path, final String encoding, final String data, final boolean transformFile, final boolean append, final Promise promise) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.writeFile(path, encoding, data, transformFile, append, promise); + } + }); + } + + public void lstat(String path, Callback callback) { + ReactNativeBlobUtilFS.lstat(path, callback); + } + + public void stat(String path, Callback callback) { + ReactNativeBlobUtilFS.stat(path, callback); + } + + public void scanFile(final ReadableArray pairs, final Callback callback) { + final ReactApplicationContext ctx = RCTContext; + threadPool.execute(new Runnable() { + @Override + public void run() { + int size = pairs.size(); + String[] p = new String[size]; + String[] m = new String[size]; + for (int i = 0; i < size; i++) { + ReadableMap pair = pairs.getMap(i); + if (pair.hasKey("path")) { + p[i] = pair.getString("path"); + if (pair.hasKey("mime")) + m[i] = pair.getString("mime"); + else + m[i] = null; + } + } + new ReactNativeBlobUtilFS(ctx).scanFile(p, m, callback); + } + }); + } + + public void hash(final String path, final String algorithm, final Promise promise) { + threadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.hash(path, algorithm, promise); + } + }); + } + + /** + * @param path Stream file path + * @param encoding Stream encoding, should be one of `base64`, `ascii`, and `utf8` + * @param bufferSize Stream buffer size, default to 4096 or 4095(base64). + */ + public void readStream(final String path, final String encoding, final int bufferSize, final int tick, final String streamId) { + final ReactApplicationContext ctx = RCTContext; + fsThreadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilStream fs = new ReactNativeBlobUtilStream(ctx); + fs.readStream(path, encoding, bufferSize, tick, streamId, RCTContext); + } + }); + } + + public void cancelRequest(String taskId, Callback callback) { + try { + ReactNativeBlobUtilReq.cancelTask(taskId); + callback.invoke(null, taskId); + } catch (Exception ex) { + callback.invoke(ex.getLocalizedMessage(), null); + } + } + + public void slice(String src, String dest, long start, long end, Promise promise) { + ReactNativeBlobUtilFS.slice(src, dest, start, end, "", promise); + } + + public void enableProgressReport(String taskId, int interval, int count) { + ReactNativeBlobUtilProgressConfig config = new ReactNativeBlobUtilProgressConfig(true, interval, count, ReactNativeBlobUtilProgressConfig.ReportType.Download); + ReactNativeBlobUtilReq.progressReport.put(taskId, config); + } + + public void df(final Callback callback) { + fsThreadPool.execute(new Runnable() { + @Override + public void run() { + ReactNativeBlobUtilFS.df(callback, RCTContext); + } + }); + } + + + public void enableUploadProgressReport(String taskId, int interval, int count) { + ReactNativeBlobUtilProgressConfig config = new ReactNativeBlobUtilProgressConfig(true, interval, count, ReactNativeBlobUtilProgressConfig.ReportType.Upload); + ReactNativeBlobUtilReq.uploadProgressReport.put(taskId, config); + } + + public void fetchBlob(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, final Callback callback) { + new ReactNativeBlobUtilReq(options, taskId, method, url, headers, body, null, mClient, callback).run(); + } + + public void fetchBlobForm(ReadableMap options, String taskId, String method, String url, ReadableMap headers, ReadableArray body, final Callback callback) { + new ReactNativeBlobUtilReq(options, taskId, method, url, headers, null, body, mClient, callback).run(); + } + + public void getContentIntent(String mime, Promise promise) { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + if (mime != null) + i.setType(mime); + else + i.setType("*/*"); + promiseTable.put(GET_CONTENT_INTENT, promise); + RCTContext.startActivityForResult(i, GET_CONTENT_INTENT, null); + + } + + public void addCompleteDownload(ReadableMap config, Promise promise) { + DownloadManager dm = (DownloadManager) RCTContext.getSystemService(RCTContext.DOWNLOAD_SERVICE); + if (config == null || !config.hasKey("path")) { + promise.reject("EINVAL", "ReactNativeBlobUtil.addCompleteDownload config or path missing."); + return; + } + String path = ReactNativeBlobUtilUtils.normalizePath(config.getString("path")); + if (path == null) { + promise.reject("EINVAL", "ReactNativeBlobUtil.addCompleteDownload can not resolve URI:" + config.getString("path")); + return; + } + try { + WritableMap stat = ReactNativeBlobUtilFS.statFile(path); + dm.addCompletedDownload( + config.hasKey("title") ? config.getString("title") : "", + config.hasKey("description") ? config.getString("description") : "", + true, + config.hasKey("mime") ? config.getString("mime") : null, + path, + Long.valueOf(stat.getString("size")), + config.hasKey("showNotification") && config.getBoolean("showNotification") + ); + promise.resolve(null); + } catch (Exception ex) { + promise.reject("EUNSPECIFIED", ex.getLocalizedMessage()); + } + + } + + public void getSDCardDir(Promise promise) { + ReactNativeBlobUtilFS.getSDCardDir(RCTContext, promise); + } + + public void getSDCardApplicationDir(Promise promise) { + ReactNativeBlobUtilFS.getSDCardApplicationDir(RCTContext, promise); + } + + public void createMediaFile(ReadableMap filedata, String mt, Promise promise) { + if (!(filedata.hasKey("name") && filedata.hasKey("parentFolder") && filedata.hasKey("mimeType"))) { + promise.reject("ReactNativeBlobUtil.createMediaFile", "invalid filedata: " + filedata.toString()); + return; + } + if (mt == null) promise.reject("ReactNativeBlobUtil.createMediaFile", "invalid mediatype"); + + FileDescription file = new FileDescription(filedata.getString("name"), filedata.getString("mimeType"), filedata.getString("parentFolder")); + Uri res = ReactNativeBlobUtilMediaCollection.createNewMediaFile(file, ReactNativeBlobUtilMediaCollection.MediaType.valueOf(mt), RCTContext); + if (res != null) promise.resolve(res.toString()); + else promise.reject("ReactNativeBlobUtil.createMediaFile", "File could not be created"); + } + + public void writeToMediaFile(String fileUri, String path, boolean transformFile, Promise promise) { + boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(Uri.parse(fileUri), path, transformFile, promise, RCTContext); + if (res) promise.resolve("Success"); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public void copyToInternal(String contentUri, String destpath, Promise promise) { + ReactNativeBlobUtilMediaCollection.copyToInternal(Uri.parse(contentUri), destpath, promise); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public void getBlob(String contentUri, String encoding, Promise promise) { + ReactNativeBlobUtilMediaCollection.getBlob(Uri.parse(contentUri), encoding, promise); + } + + public void copyToMediaStore(ReadableMap filedata, String mt, String path, Promise promise) { + if (!(filedata.hasKey("name") && filedata.hasKey("parentFolder") && filedata.hasKey("mimeType"))) { + promise.reject("ReactNativeBlobUtil.createMediaFile", "invalid filedata: " + filedata.toString()); + return; + } + if (mt == null) { + promise.reject("ReactNativeBlobUtil.createMediaFile", "invalid mediatype"); + return; + } + if (path == null) { + promise.reject("ReactNativeBlobUtil.createMediaFile", "invalid path"); + return; + } + + FileDescription file = new FileDescription(filedata.getString("name"), filedata.getString("mimeType"), filedata.getString("parentFolder")); + Uri fileuri = ReactNativeBlobUtilMediaCollection.createNewMediaFile(file, ReactNativeBlobUtilMediaCollection.MediaType.valueOf(mt), RCTContext); + + if (fileuri == null) { + promise.reject("ReactNativeBlobUtil.createMediaFile", "File could not be created"); + return; + } + + boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, false, promise, RCTContext); + if (res) promise.resolve(fileuri.toString()); + } + +} diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilMediaCollection.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilMediaCollection.java new file mode 100644 index 000000000..12434859a --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilMediaCollection.java @@ -0,0 +1,318 @@ +package com.ReactNativeBlobUtil; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; +import android.util.Base64; + +import com.ReactNativeBlobUtil.Utils.FileDescription; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableArray; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ReactNativeBlobUtilMediaCollection { + + public enum MediaType { + Audio, + Image, + Video, + Download + } + + private static Uri getMediaUri(MediaType mt) { + Uri res = null; + if (mt == MediaType.Audio) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } else { + res = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + } else if (mt == MediaType.Video) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } else { + res = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } + } else if (mt == MediaType.Image) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } else { + res = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } + } else if (mt == MediaType.Download) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } + } + + return res; + } + + private static String getRelativePath(MediaType mt, ReactApplicationContext ctx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (mt == MediaType.Audio) return Environment.DIRECTORY_MUSIC; + if (mt == MediaType.Video) return Environment.DIRECTORY_MOVIES; + if (mt == MediaType.Image) return Environment.DIRECTORY_PICTURES; + if (mt == MediaType.Download) return Environment.DIRECTORY_DOWNLOADS; + return Environment.DIRECTORY_DOWNLOADS; + } else { + if (mt == MediaType.Audio) return ReactNativeBlobUtilFS.getLegacySystemfolders(ctx).get("LegacyMusicDir").toString(); + if (mt == MediaType.Video) return ReactNativeBlobUtilFS.getLegacySystemfolders(ctx).get("LegacyMovieDir").toString(); + if (mt == MediaType.Image) return ReactNativeBlobUtilFS.getLegacySystemfolders(ctx).get("LegacyPictureDir").toString(); + if (mt == MediaType.Download) return ReactNativeBlobUtilFS.getLegacySystemfolders(ctx).get("LegacyDownloadDir").toString(); + return ReactNativeBlobUtilFS.getLegacySystemfolders(ctx).get("LegacyDownloadDir").toString(); + } + } + + public static Uri createNewMediaFile(FileDescription file, MediaType mt, ReactApplicationContext ctx) { + // Add a specific media item. + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + + ContentValues fileDetails = new ContentValues(); + String relativePath = getRelativePath(mt, ctx); + String mimeType = file.mimeType; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + fileDetails.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); + fileDetails.put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); + fileDetails.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); + fileDetails.put(MediaStore.MediaColumns.DISPLAY_NAME, file.name); + fileDetails.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath + '/' + file.partentFolder); + + Uri mediauri = getMediaUri(mt); + + try { + // Keeps a handle to the new file's URI in case we need to modify it later. + return resolver.insert(mediauri, fileDetails); + } catch (Exception e) { + return null; + } + } else { + File f = new File(relativePath + file.getFullPath()); + if (true) { + if (!f.exists()) { + File parent = f.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + return null; + } + try { + if (f.createNewFile()) ; + { + return Uri.fromFile(f); + } + } catch (IOException ioException) { + return null; + } + + } else { + return Uri.fromFile(f); + } + + } + } + + return null; + } + + public static boolean writeToMediaFile(Uri fileUri, String data, boolean transformFile, Promise promise, ReactApplicationContext ctx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + Context appCtx = ctx.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + + // set pending doesn't work right now. We would have to requery for the item + //ContentValues contentValues = new ContentValues(); + //contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1); + //resolver.update(fileUri, contentValues, null, null); + + // write data + OutputStream stream = null; + Uri uri = null; + + try { + ParcelFileDescriptor descr; + try { + assert fileUri != null; + descr = appCtx.getContentResolver().openFileDescriptor(fileUri, "w"); + assert descr != null; + String normalizedData = ReactNativeBlobUtilUtils.normalizePath(data); + File src = new File(normalizedData); + if (!src.exists()) { + promise.reject("ENOENT", "No such file ('" + normalizedData + "')"); + return false; + } + + + FileInputStream fin = new FileInputStream(src); + FileOutputStream out = new FileOutputStream(descr.getFileDescriptor()); + + if (transformFile) { + // in order to transform file, we must load the entire file onto memory + int length = (int) src.length(); + byte[] bytes = new byte[length]; + fin.read(bytes); + if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) { + throw new IllegalStateException("Write to media file with transform was specified but the shared file transformer is not set"); + } + byte[] transformedBytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(bytes); + out.write(transformedBytes); + } else { + byte[] buf = new byte[10240]; + int read; + + while ((read = fin.read(buf)) > 0) { + out.write(buf, 0, read); + } + } + + + fin.close(); + out.close(); + descr.close(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new IOException("Failed to get output stream.")); + return false; + } + + //contentValues.clear(); + //contentValues.put(MediaStore.Video.Media.IS_PENDING, 0); + //appCtx.getContentResolver().update(fileUri, contentValues, null, null); + stream = resolver.openOutputStream(fileUri); + if (stream == null) { + promise.reject(new IOException("Failed to get output stream.")); + return false; + } + } catch (IOException e) { + // Don't leave an orphan entry in the MediaStore + resolver.delete(uri, null, null); + promise.reject(e); + return false; + } finally { + if (stream != null) { + stream.close(); + } + } + + // remove pending + //contentValues = new ContentValues(); + //contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0); + //resolver.update(fileUri, contentValues, null, null); + + } catch (IOException e) { + promise.reject("ReactNativeBlobUtil.createMediaFile", "Cannot write to file, file might not exist"); + return false; + } + return true; + } else { + return ReactNativeBlobUtilFS.writeFile(ReactNativeBlobUtilUtils.normalizePath(fileUri.toString()), ReactNativeBlobUtilConst.DATA_ENCODE_URI, data, false); + } + } + + public static void copyToInternal(Uri contenturi, String destpath, Promise promise) { + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + + InputStream in = null; + OutputStream out = null; + File f = new File((destpath)); + + if (!f.exists()) { + try { + File parent = f.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + promise.reject("ReactNativeBlobUtil.copyToInternal: Cannot create parent folders<'" + destpath); + return; + } + boolean result = f.createNewFile(); + if (!result) { + promise.reject("ReactNativeBlobUtil.copyToInternal: Destination file at '" + destpath + "' already exists"); + return; + } + } catch (IOException ioException) { + promise.reject("ReactNativeBlobUtil.copyToInternal: Could not create file: " + ioException.getLocalizedMessage()); + } + } + + try { + in = resolver.openInputStream(contenturi); + out = new FileOutputStream(destpath); + + byte[] buf = new byte[10240]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + + } catch (IOException e) { + promise.reject("ReactNativeBlobUtil.copyToInternal: Could not write data: " + e.getLocalizedMessage()); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } + if (out != null) { + try { + out.close(); + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } + } + + promise.resolve(""); + } + + public static void getBlob(Uri contentUri, String encoding, Promise promise) { + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + try { + InputStream in = resolver.openInputStream(contentUri); + int length = in.available(); + + byte[] bytes = new byte[length]; + int bytesRead = in.read(bytes); + in.close(); + + if (bytesRead < length) { + promise.reject("EUNSPECIFIED", "Read only " + bytesRead + " bytes of " + length); + return; + } + + switch (encoding.toLowerCase()) { + case "base64": + promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP)); + break; + case "ascii": + WritableArray asciiResult = Arguments.createArray(); + for (byte b : bytes) { + asciiResult.pushInt(b); + } + promise.resolve(asciiResult); + break; + default: // covers utf-8 + promise.resolve(new String(bytes)); + break; + } + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } +} diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilPackage.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilPackage.java new file mode 100644 index 000000000..3d74aa4f6 --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilPackage.java @@ -0,0 +1,47 @@ +package com.ReactNativeBlobUtil; + +import androidx.annotation.Nullable; + +import com.facebook.react.BaseReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; + +import java.util.HashMap; +import java.util.Map; + +// trick autolinking till it is fixed on RN side +//public class ReactNativeBlobUtilPackage extends TurboReactPackage { +public class ReactNativeBlobUtilPackage extends BaseReactPackage { + + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + if (name.equals(ReactNativeBlobUtilImpl.NAME)) { + return new ReactNativeBlobUtil(reactContext); + } else { + return null; + } + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + final Map moduleInfos = new HashMap<>(); + boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + moduleInfos.put( + ReactNativeBlobUtilImpl.NAME, + new ReactModuleInfo( + ReactNativeBlobUtilImpl.NAME, + ReactNativeBlobUtilImpl.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // isCxxModule + isTurboModule // isTurboModule + )); + return moduleInfos; + }; + } + +} diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobProgressConfig.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilProgressConfig.java similarity index 60% rename from android/src/main/java/com/RNFetchBlob/RNFetchBlobProgressConfig.java rename to android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilProgressConfig.java index 6b97bef9e..69adb38d1 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobProgressConfig.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilProgressConfig.java @@ -1,14 +1,14 @@ -package com.RNFetchBlob; +package com.ReactNativeBlobUtil; /** * Created by wkh237 on 2016/9/24. */ -public class RNFetchBlobProgressConfig { +public class ReactNativeBlobUtilProgressConfig { enum ReportType { Upload, Download - }; + } private long lastTick = 0; private int tick = 0; @@ -17,7 +17,7 @@ enum ReportType { private boolean enable = false; private ReportType type = ReportType.Download; - RNFetchBlobProgressConfig(boolean report, int interval, int count, ReportType type) { + ReactNativeBlobUtilProgressConfig(boolean report, int interval, int count, ReportType type) { this.enable = report; this.interval = interval; this.type = type; @@ -26,10 +26,10 @@ enum ReportType { public boolean shouldReport(float progress) { boolean checkCount = true; - if(count > 0 && progress > 0) - checkCount = Math.floor(progress*count)> tick; - boolean result = (System.currentTimeMillis() - lastTick> interval) && enable && checkCount; - if(result) { + if (count > 0 && progress > 0) + checkCount = Math.floor(progress * count) > tick; + boolean result = (System.currentTimeMillis() - lastTick > interval) && enable && checkCount; + if (result) { tick++; lastTick = System.currentTimeMillis(); } diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java similarity index 51% rename from android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java rename to android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java index 5fde54097..ddf5adb63 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java @@ -1,4 +1,4 @@ -package com.RNFetchBlob; +package com.ReactNativeBlobUtil; import android.app.DownloadManager; import android.content.BroadcastReceiver; @@ -6,17 +6,25 @@ import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import androidx.annotation.NonNull; +import android.net.ConnectivityManager; import android.net.Network; -import android.net.NetworkInfo; import android.net.NetworkCapabilities; -import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; import android.util.Base64; +import android.webkit.CookieManager; + +import androidx.annotation.NonNull; -import com.RNFetchBlob.Response.RNFetchBlobDefaultResp; -import com.RNFetchBlob.Response.RNFetchBlobFileResp; +import com.ReactNativeBlobUtil.Response.ReactNativeBlobUtilDefaultResp; +import com.ReactNativeBlobUtil.Response.ReactNativeBlobUtilFileResp; +import com.ReactNativeBlobUtil.Utils.Tls12SocketFactory; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilFS; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; @@ -25,35 +33,35 @@ import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.modules.core.DeviceEventManagerModule; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import javax.net.ssl.SSLContext; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.Proxy; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URL; -import java.net.Proxy; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.security.KeyStore; +import java.nio.charset.CharsetDecoder; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.HashMap; +import java.util.UUID; + +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.SSLContext; import okhttp3.Call; import okhttp3.ConnectionPool; @@ -69,9 +77,9 @@ import okhttp3.TlsVersion; -public class RNFetchBlobReq extends BroadcastReceiver implements Runnable { +public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnable { - enum RequestType { + enum RequestType { Form, SingleFile, AsIs, @@ -90,24 +98,31 @@ enum ResponseFormat { BASE64 } + private boolean shouldTransformFile() { + return this.options.transformFile && + // Can only process if it's written to a file + (this.options.fileCache || this.options.path != null); + } + public static HashMap taskTable = new HashMap<>(); public static HashMap androidDownloadManagerTaskTable = new HashMap<>(); - static HashMap progressReport = new HashMap<>(); - static HashMap uploadProgressReport = new HashMap<>(); + static HashMap progressReport = new HashMap<>(); + static HashMap uploadProgressReport = new HashMap<>(); static ConnectionPool pool = new ConnectionPool(); - RNFetchBlobConfig options; + ReactNativeBlobUtilConfig options; String taskId; String method; String url; String rawRequestBody; String destPath; + String customPath; ReadableArray rawRequestBodyArray; ReadableMap headers; Callback callback; long contentLength; long downloadManagerId; - RNFetchBlobBody requestBody; + ReactNativeBlobUtilBody requestBody; RequestType requestType; ResponseType responseType; ResponseFormat responseFormat = ResponseFormat.Auto; @@ -115,10 +130,11 @@ enum ResponseFormat { boolean timeout = false; ArrayList redirects = new ArrayList<>(); OkHttpClient client; + boolean callbackfired; - public RNFetchBlobReq(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, ReadableArray arrayBody, OkHttpClient client, final Callback callback) { - this.method = method.toUpperCase(); - this.options = new RNFetchBlobConfig(options); + public ReactNativeBlobUtilReq(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, ReadableArray arrayBody, OkHttpClient client, final Callback callback) { + this.method = method.toUpperCase(Locale.ROOT); + this.options = new ReactNativeBlobUtilConfig(options); this.taskId = taskId; this.url = url; this.headers = headers; @@ -126,8 +142,11 @@ public RNFetchBlobReq(ReadableMap options, String taskId, String method, String this.rawRequestBody = body; this.rawRequestBodyArray = arrayBody; this.client = client; + this.callbackfired = false; - if(this.options.fileCache || this.options.path != null) + // If transformFile is specified, we first want to get the response back in memory so we can + // encrypt it wholesale and at that point, write it into the file storage. + if((this.options.fileCache || this.options.path != null) && !this.shouldTransformFile()) responseType = ResponseType.FileStorage; else responseType = ResponseType.KeepInMemory; @@ -142,61 +161,192 @@ else if (arrayBody != null) } public static void cancelTask(String taskId) { - if(taskTable.containsKey(taskId)) { - Call call = taskTable.get(taskId); + Call call = taskTable.get(taskId); + if (call != null) { call.cancel(); taskTable.remove(taskId); } if (androidDownloadManagerTaskTable.containsKey(taskId)) { long downloadManagerIdForTaskId = androidDownloadManagerTaskTable.get(taskId).longValue(); - Context appCtx = RNFetchBlob.RCTContext.getApplicationContext(); + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE); dm.remove(downloadManagerIdForTaskId); } } + private void invoke_callback(Object... args) { + if (this.callbackfired) return; + this.callback.invoke(args); + this.callbackfired = true; + } + + private final int QUERY = 1314; + private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); + private Future future; + private Handler mHandler = new Handler(new Handler.Callback() { + public boolean handleMessage(Message msg) { + switch (msg.what) { + + case QUERY: + + Bundle data = msg.getData(); + long id = data.getLong("downloadManagerId"); + if (id == downloadManagerId) { + + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); + + DownloadManager downloadManager = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE); + + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadManagerId); + + Cursor cursor = downloadManager.query(query); + + if (cursor != null && cursor.moveToFirst()) { + + long written = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); + + long total = cursor.getLong(cursor.getColumnIndex( + DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); + cursor.close(); + + ReactNativeBlobUtilProgressConfig reportConfig = getReportProgress(taskId); + float progress = (total > 0) ? written / total : 0; + + if (reportConfig != null && reportConfig.shouldReport(progress /* progress */)) { + WritableMap args = Arguments.createMap(); + args.putString("taskId", String.valueOf(taskId)); + args.putString("written", String.valueOf(written)); + args.putString("total", String.valueOf(total)); + args.putString("chunk", ""); + ReactNativeBlobUtilImpl.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(ReactNativeBlobUtilConst.EVENT_PROGRESS, args); + + } + + if (total == written) { + future.cancel(true); + } + } + } + } + return true; + } + }); + @Override public void run() { - + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); // use download manager instead of default HTTP implementation if (options.addAndroidDownloads != null && options.addAndroidDownloads.hasKey("useDownloadManager")) { if (options.addAndroidDownloads.getBoolean("useDownloadManager")) { Uri uri = Uri.parse(url); DownloadManager.Request req = new DownloadManager.Request(uri); - if(options.addAndroidDownloads.hasKey("notification") && options.addAndroidDownloads.getBoolean("notification")) { + if (options.addAndroidDownloads.hasKey("notification") && options.addAndroidDownloads.getBoolean("notification")) { req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); } else { req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); } - if(options.addAndroidDownloads.hasKey("title")) { + if (options.addAndroidDownloads.hasKey("title")) { req.setTitle(options.addAndroidDownloads.getString("title")); } - if(options.addAndroidDownloads.hasKey("description")) { + if (options.addAndroidDownloads.hasKey("description")) { req.setDescription(options.addAndroidDownloads.getString("description")); } - if(options.addAndroidDownloads.hasKey("path")) { - req.setDestinationUri(Uri.parse("file://" + options.addAndroidDownloads.getString("path"))); + if (options.addAndroidDownloads.hasKey("path")) { + String path = options.addAndroidDownloads.getString("path"); + File f = new File(path); + File dir = f.getParentFile(); + + if (!f.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs() && !dir.exists()) { + invoke_callback( "Failed to create parent directory of '" + path + "'", null, null); + return; + } + } + } + req.setDestinationUri(Uri.parse("file://" + path)); + + customPath = path; + } + + + if (options.addAndroidDownloads.hasKey("storeLocal") && options.addAndroidDownloads.getBoolean("storeLocal")) { + String path = (String) ReactNativeBlobUtilFS.getSystemfolders(ReactNativeBlobUtilImpl.RCTContext).get("DownloadDir"); + path = path + UUID.randomUUID().toString(); + + File f = new File(path); + File dir = f.getParentFile(); + if (!f.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs() && !dir.exists()) { + invoke_callback( "Failed to create parent directory of '" + path + "'", null, null); + return; + } + } + } + req.setDestinationUri(Uri.parse("file://" + path)); + customPath = path; } - // #391 Add MIME type to the request - if(options.addAndroidDownloads.hasKey("mime")) { + + if (options.addAndroidDownloads.hasKey("mime")) { req.setMimeType(options.addAndroidDownloads.getString("mime")); } - // set headers - ReadableMapKeySetIterator it = headers.keySetIterator(); - if(options.addAndroidDownloads.hasKey("mediaScannable") && options.addAndroidDownloads.hasKey("mediaScannable")) { + + if (options.addAndroidDownloads.hasKey("mediaScannable") && options.addAndroidDownloads.getBoolean("mediaScannable")) { req.allowScanningByMediaScanner(); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.addAndroidDownloads.hasKey("storeInDownloads") && options.addAndroidDownloads.getBoolean("storeInDownloads")) { + String t = options.addAndroidDownloads.getString("title"); + if(t == null || t.isEmpty()) + t = UUID.randomUUID().toString(); + if(this.options.appendExt != null && !this.options.appendExt.isEmpty()) + t += "." + this.options.appendExt; + + req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, t); + } + + // set headers + ReadableMapKeySetIterator it = headers.keySetIterator(); while (it.hasNextKey()) { String key = it.nextKey(); req.addRequestHeader(key, headers.getString(key)); } - Context appCtx = RNFetchBlob.RCTContext.getApplicationContext(); + + // Attempt to add cookie, if it exists + URL urlObj; + try { + urlObj = new URL(url); + String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost(); + String cookie = CookieManager.getInstance().getCookie(baseUrl); + req.addRequestHeader("Cookie", cookie); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE); downloadManagerId = dm.enqueue(req); androidDownloadManagerTaskTable.put(taskId, Long.valueOf(downloadManagerId)); - appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + if(Build.VERSION.SDK_INT >= 34 ){ + appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED); + }else{ + appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + } + future = scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + Message msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putLong("downloadManagerId", downloadManagerId); + msg.setData(data); + msg.what = QUERY; + mHandler.sendMessage(msg); + } + }, 0, 100, TimeUnit.MILLISECONDS); return; } @@ -204,26 +354,26 @@ public void run() { // find cached result if `key` property exists String cacheKey = this.taskId; - String ext = this.options.appendExt.isEmpty() ? "" : "." + this.options.appendExt; + String ext = (this.options.appendExt == null || this.options.appendExt.isEmpty()) ? "" : "." + this.options.appendExt; if (this.options.key != null) { - cacheKey = RNFetchBlobUtils.getMD5(this.options.key); + cacheKey = ReactNativeBlobUtilUtils.getMD5(this.options.key); if (cacheKey == null) { cacheKey = this.taskId; } - File file = new File(RNFetchBlobFS.getTmpPath(cacheKey) + ext); + File file = new File(ReactNativeBlobUtilFS.getTmpPath(cacheKey) + ext); if (file.exists()) { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, file.getAbsolutePath()); + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, file.getAbsolutePath()); return; } } - if(this.options.path != null) + if (this.options.path != null) this.destPath = this.options.path; - else if(this.options.fileCache) - this.destPath = RNFetchBlobFS.getTmpPath(cacheKey) + ext; + else if (this.options.fileCache) + this.destPath = ReactNativeBlobUtilFS.getTmpPath(cacheKey) + ext; OkHttpClient.Builder clientBuilder; @@ -231,19 +381,19 @@ else if(this.options.fileCache) try { // use trusty SSL socket if (this.options.trusty) { - clientBuilder = RNFetchBlobUtils.getUnsafeOkHttpClient(client); + clientBuilder = ReactNativeBlobUtilUtils.getUnsafeOkHttpClient(client); } else { clientBuilder = client.newBuilder(); } // wifi only, need ACCESS_NETWORK_STATE permission // and API level >= 21 - if(this.options.wifiOnly){ + if (this.options.wifiOnly) { boolean found = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - ConnectivityManager connectivityManager = (ConnectivityManager) RNFetchBlob.RCTContext.getSystemService(RNFetchBlob.RCTContext.CONNECTIVITY_SERVICE); + ConnectivityManager connectivityManager = (ConnectivityManager) ReactNativeBlobUtilImpl.RCTContext.getSystemService(ReactNativeBlobUtilImpl.RCTContext.CONNECTIVITY_SERVICE); Network[] networks = connectivityManager.getAllNetworks(); for (Network network : networks) { @@ -251,15 +401,15 @@ else if(this.options.fileCache) NetworkInfo netInfo = connectivityManager.getNetworkInfo(network); NetworkCapabilities caps = connectivityManager.getNetworkCapabilities(network); - if(caps == null || netInfo == null){ + if (caps == null || netInfo == null) { continue; } - if(!netInfo.isConnected()){ + if (!netInfo.isConnected()) { continue; } - if(caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)){ + if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { clientBuilder.proxy(Proxy.NO_PROXY); clientBuilder.socketFactory(network.getSocketFactory()); found = true; @@ -268,14 +418,13 @@ else if(this.options.fileCache) } } - if(!found){ - callback.invoke("No available WiFi connections.", null, null); + if (!found) { + invoke_callback("No available WiFi connections.", null, null); releaseTaskResource(); return; } - } - else{ - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob: wifiOnly was set, but SDK < 21. wifiOnly was ignored."); + } else { + ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil: wifiOnly was set, but SDK < 21. wifiOnly was ignored."); } } @@ -293,49 +442,45 @@ else if(this.options.fileCache) while (it.hasNextKey()) { String key = it.nextKey(); String value = headers.getString(key); - if(key.equalsIgnoreCase("RNFB-Response")) { - if(value.equalsIgnoreCase("base64")) + if (key.equalsIgnoreCase("RNFB-Response")) { + if (value.equalsIgnoreCase("base64")) responseFormat = ResponseFormat.BASE64; else if (value.equalsIgnoreCase("utf8")) responseFormat = ResponseFormat.UTF8; - } - else { - builder.header(key.toLowerCase(), value); - mheaders.put(key.toLowerCase(), value); + } else { + builder.header(key.toLowerCase(Locale.ROOT), value); + mheaders.put(key.toLowerCase(Locale.ROOT), value); } } } - if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) { - String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase(); + if (method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) { + String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase(Locale.ROOT); - if(rawRequestBodyArray != null) { + if (rawRequestBodyArray != null) { requestType = RequestType.Form; - } - else if(cType.isEmpty()) { - if(!cType.equalsIgnoreCase("")) { - builder.header("Content-Type", "application/octet-stream"); + } else if (cType == null || cType.isEmpty()) { + if (!cType.equalsIgnoreCase("")) { + builder.header("Content-Type", "application/octet-stream"); } requestType = RequestType.SingleFile; } - if(rawRequestBody != null) { - if (rawRequestBody.startsWith(RNFetchBlobConst.FILE_PREFIX) - || rawRequestBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { + if (rawRequestBody != null) { + if (rawRequestBody.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX) + || rawRequestBody.startsWith(ReactNativeBlobUtilConst.CONTENT_PREFIX)) { requestType = RequestType.SingleFile; - } - else if (cType.toLowerCase().contains(";base64") || cType.toLowerCase().startsWith("application/octet")) { - cType = cType.replace(";base64","").replace(";BASE64",""); - if(mheaders.containsKey("content-type")) + } else if (cType.toLowerCase(Locale.ROOT).contains(";base64") || cType.toLowerCase(Locale.ROOT).startsWith("application/octet")) { + cType = cType.replace(";base64", "").replace(";BASE64", ""); + if (mheaders.containsKey("content-type")) mheaders.put("content-type", cType); - if(mheaders.containsKey("Content-Type")) + if (mheaders.containsKey("Content-Type")) mheaders.put("Content-Type", cType); requestType = RequestType.SingleFile; } else { requestType = RequestType.AsIs; } } - } - else { + } else { requestType = RequestType.WithoutBody; } @@ -344,7 +489,7 @@ else if(cType.isEmpty()) { // set request body switch (requestType) { case SingleFile: - requestBody = new RNFetchBlobBody(taskId) + requestBody = new ReactNativeBlobUtilBody(taskId) .chunkedEncoding(isChunkedRequest) .setRequestType(requestType) .setBody(rawRequestBody) @@ -352,7 +497,7 @@ else if(cType.isEmpty()) { builder.method(method, requestBody); break; case AsIs: - requestBody = new RNFetchBlobBody(taskId) + requestBody = new ReactNativeBlobUtilBody(taskId) .chunkedEncoding(isChunkedRequest) .setRequestType(requestType) .setBody(rawRequestBody) @@ -360,21 +505,19 @@ else if(cType.isEmpty()) { builder.method(method, requestBody); break; case Form: - String boundary = "RNFetchBlob-" + taskId; - requestBody = new RNFetchBlobBody(taskId) + String boundary = "ReactNativeBlobUtil-" + taskId; + requestBody = new ReactNativeBlobUtilBody(taskId) .chunkedEncoding(isChunkedRequest) .setRequestType(requestType) .setBody(rawRequestBodyArray) - .setMIME(MediaType.parse("multipart/form-data; boundary="+ boundary)); + .setMIME(MediaType.parse("multipart/form-data; boundary=" + boundary)); builder.method(method, requestBody); break; case WithoutBody: - if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) - { + if (method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) { builder.method(method, RequestBody.create(null, new byte[0])); - } - else + } else builder.method(method, null); break; } @@ -382,60 +525,70 @@ else if(cType.isEmpty()) { // #156 fix cookie issue final Request req = builder.build(); clientBuilder.addNetworkInterceptor(new Interceptor() { + @NonNull @Override - public Response intercept(Chain chain) throws IOException { + public Response intercept(@NonNull Chain chain) throws IOException { redirects.add(chain.request().url().toString()); return chain.proceed(chain.request()); } }); // Add request interceptor for upload progress event clientBuilder.addInterceptor(new Interceptor() { + @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { + Response originalResponse = null; try { - Response originalResponse = chain.proceed(req); + originalResponse = chain.proceed(req); ResponseBody extended; switch (responseType) { case KeepInMemory: - extended = new RNFetchBlobDefaultResp( - RNFetchBlob.RCTContext, + extended = new ReactNativeBlobUtilDefaultResp( + ReactNativeBlobUtilImpl.RCTContext, taskId, originalResponse.body(), options.increment); break; case FileStorage: - extended = new RNFetchBlobFileResp( - RNFetchBlob.RCTContext, + extended = new ReactNativeBlobUtilFileResp( + ReactNativeBlobUtilImpl.RCTContext, taskId, originalResponse.body(), destPath, options.overwrite); break; default: - extended = new RNFetchBlobDefaultResp( - RNFetchBlob.RCTContext, + extended = new ReactNativeBlobUtilDefaultResp( + ReactNativeBlobUtilImpl.RCTContext, taskId, originalResponse.body(), options.increment); break; } return originalResponse.newBuilder().body(extended).build(); - } - catch(SocketException e) { + } catch (SocketException e) { timeout = true; - } - catch (SocketTimeoutException e ){ + if (originalResponse != null) { + originalResponse.close(); + } + } catch (SocketTimeoutException e) { timeout = true; - //RNFetchBlobUtils.emitWarningEvent("RNFetchBlob error when sending request : " + e.getLocalizedMessage()); - } catch(Exception ex) { - + if (originalResponse != null) { + originalResponse.close(); + } + //ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil error when sending request : " + e.getLocalizedMessage()); + } catch (Exception ex) { + if (originalResponse != null) { + originalResponse.close(); + } } + return chain.proceed(chain.request()); } }); - if(options.timeout >= 0) { + if (options.timeout >= 0) { clientBuilder.connectTimeout(options.timeout, TimeUnit.MILLISECONDS); clientBuilder.readTimeout(options.timeout, TimeUnit.MILLISECONDS); } @@ -448,24 +601,23 @@ public Response intercept(@NonNull Chain chain) throws IOException { OkHttpClient client = enableTls12OnPreLollipop(clientBuilder).build(); - Call call = client.newCall(req); + Call call = client.newCall(req); taskTable.put(taskId, call); call.enqueue(new okhttp3.Callback() { @Override - public void onFailure(@NonNull Call call, IOException e) { + public void onFailure(@NonNull Call call, @NonNull IOException e) { cancelTask(taskId); - if(respInfo == null) { + if (respInfo == null) { respInfo = Arguments.createMap(); } // check if this error caused by socket timeout - if(e.getClass().equals(SocketTimeoutException.class)) { + if (e.getClass().equals(SocketTimeoutException.class)) { respInfo.putBoolean("timeout", true); - callback.invoke("The request timed out.", null, null); - } - else - callback.invoke(e.getLocalizedMessage(), null, null); + invoke_callback("The request timed out.", null, null); + } else + invoke_callback(e.getLocalizedMessage(), null, null); releaseTaskResource(); } @@ -473,20 +625,20 @@ public void onFailure(@NonNull Call call, IOException e) { public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { ReadableMap notifyConfig = options.addAndroidDownloads; // Download manager settings - if(notifyConfig != null ) { + if (notifyConfig != null) { String title = "", desc = "", mime = "text/plain"; boolean scannable = false, notification = false; - if(notifyConfig.hasKey("title")) + if (notifyConfig.hasKey("title")) title = options.addAndroidDownloads.getString("title"); - if(notifyConfig.hasKey("description")) + if (notifyConfig.hasKey("description")) desc = notifyConfig.getString("description"); - if(notifyConfig.hasKey("mime")) + if (notifyConfig.hasKey("mime")) mime = notifyConfig.getString("mime"); - if(notifyConfig.hasKey("mediaScannable")) + if (notifyConfig.hasKey("mediaScannable")) scannable = notifyConfig.getBoolean("mediaScannable"); - if(notifyConfig.hasKey("notification")) + if (notifyConfig.hasKey("notification")) notification = notifyConfig.getBoolean("notification"); - DownloadManager dm = (DownloadManager)RNFetchBlob.RCTContext.getSystemService(RNFetchBlob.RCTContext.DOWNLOAD_SERVICE); + DownloadManager dm = (DownloadManager) ReactNativeBlobUtilImpl.RCTContext.getSystemService(ReactNativeBlobUtilImpl.RCTContext.DOWNLOAD_SERVICE); dm.addCompletedDownload(title, desc, scannable, mime, destPath, contentLength, notification); } @@ -498,7 +650,7 @@ public void onResponse(@NonNull Call call, @NonNull Response response) throws IO } catch (Exception error) { error.printStackTrace(); releaseTaskResource(); - callback.invoke("RNFetchBlob request error: " + error.getMessage() + error.getCause()); + invoke_callback("ReactNativeBlobUtil request error: " + error.getMessage() + error.getCause()); } } @@ -506,32 +658,36 @@ public void onResponse(@NonNull Call call, @NonNull Response response) throws IO * Remove cached information of the HTTP task */ private void releaseTaskResource() { - if(taskTable.containsKey(taskId)) + if (taskTable.containsKey(taskId)) taskTable.remove(taskId); - if(androidDownloadManagerTaskTable.containsKey(taskId)) + if (androidDownloadManagerTaskTable.containsKey(taskId)) androidDownloadManagerTaskTable.remove(taskId); - if(uploadProgressReport.containsKey(taskId)) + if (uploadProgressReport.containsKey(taskId)) uploadProgressReport.remove(taskId); - if(progressReport.containsKey(taskId)) + if (progressReport.containsKey(taskId)) progressReport.remove(taskId); - if(requestBody != null) + if (requestBody != null) requestBody.clearRequestBody(); } /** * Send response data back to javascript context. + * * @param resp OkHttp response object */ private void done(Response resp) { boolean isBlobResp = isBlobResponse(resp); + WritableMap respmap = getResponseInfo(resp,isBlobResp); + emitStateEvent(respmap.copy()); + emitStateEvent(getResponseInfo(resp, isBlobResp)); switch (responseType) { case KeepInMemory: try { // For XMLHttpRequest, automatic response data storing strategy, when response // data is considered as binary data, write it to file system - if(isBlobResp && options.auto) { - String dest = RNFetchBlobFS.getTmpPath(taskId); + if (isBlobResp && options.auto) { + String dest = ReactNativeBlobUtilFS.getTmpPath(taskId); InputStream ins = resp.body().byteStream(); FileOutputStream os = new FileOutputStream(new File(dest)); int read; @@ -542,41 +698,57 @@ private void done(Response resp) { ins.close(); os.flush(); os.close(); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, dest); + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, dest, respmap.copy()); } // response data directly pass to JS context as string. else { - // #73 Check if the response data contains valid UTF8 string, since BASE64 - // encoding will somehow break the UTF8 string format, to encode UTF8 - // string correctly, we should do URL encoding before BASE64. byte[] b = resp.body().bytes(); - CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); - if(responseFormat == ResponseFormat.BASE64) { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP)); + // If process file option is turned on, we first keep response in memory and then stream that content + // after processing + if (this.shouldTransformFile()) { + if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) { + throw new IllegalStateException("Write file with transform was specified but the shared file transformer is not set"); + } + this.destPath = this.destPath.replace("?append=true", ""); + File file = new File(this.destPath); + if (!file.exists()) { + file.createNewFile(); + } + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(b)); + } catch (Exception e) { + invoke_callback("Error from file transformer:" + e.getLocalizedMessage(), respmap.copy()); + return; + } + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, this.destPath, respmap.copy()); + return; + } + if (responseFormat == ResponseFormat.BASE64) { + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP), respmap.copy()); return; } try { - encoder.encode(ByteBuffer.wrap(b).asCharBuffer()); - // if the data contains invalid characters the following lines will be - // skipped. - String utf8 = new String(b); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, utf8); + // Attempt to decode the incoming response data to determine whether it contains a valid UTF8 string + Charset charSet = Charset.forName("UTF-8"); + CharsetDecoder decoder = charSet.newDecoder(); + decoder.decode(ByteBuffer.wrap(b)); + // If the data contains invalid characters the following lines will be skipped. + String utf8 = new String(b, charSet); + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_UTF8, utf8); } - // This usually mean the data is contains invalid unicode characters but still valid data, + // This usually means the data contains invalid unicode characters but still valid data, // it's binary data, so send it as a normal string - catch(CharacterCodingException ignored) { - - if(responseFormat == ResponseFormat.UTF8) { + catch (CharacterCodingException ignored) { + if (responseFormat == ResponseFormat.UTF8) { String utf8 = new String(b); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, utf8); - } - else { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP)); + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_UTF8, utf8, respmap.copy()); + } else { + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP), respmap.copy()); } } } } catch (IOException e) { - callback.invoke("RNFetchBlob failed to encode response data to BASE64 string.", null); + invoke_callback("ReactNativeBlobUtil failed to encode response data to BASE64 string.", respmap.copy()); } break; case FileStorage: @@ -591,10 +763,10 @@ private void done(Response resp) { // ignored.printStackTrace(); } - RNFetchBlobFileResp rnFetchBlobFileResp; + ReactNativeBlobUtilFileResp ReactNativeBlobUtilFileResp; try { - rnFetchBlobFileResp = (RNFetchBlobFileResp) responseBody; + ReactNativeBlobUtilFileResp = (ReactNativeBlobUtilFileResp) responseBody; } catch (ClassCastException ex) { // unexpected response type if (responseBody != null) { @@ -605,30 +777,29 @@ private void done(Response resp) { if (isBufferDataExists && isContentExists) { responseBodyString = responseBody.string(); } - } catch(IOException exception) { + } catch (IOException exception) { exception.printStackTrace(); } - callback.invoke("Unexpected FileStorage response file: " + responseBodyString, null); + invoke_callback("Unexpected FileStorage response file: " + responseBodyString, respmap.copy()); } else { - callback.invoke("Unexpected FileStorage response with no file.", null); + invoke_callback("Unexpected FileStorage response with no file.", respmap.copy()); } return; } - if(rnFetchBlobFileResp != null && !rnFetchBlobFileResp.isDownloadComplete()){ - callback.invoke("Download interrupted.", null); - } - else { + if (ReactNativeBlobUtilFileResp != null && !ReactNativeBlobUtilFileResp.isDownloadComplete()) { + invoke_callback("Download interrupted.", respmap.copy()); + } else { this.destPath = this.destPath.replace("?append=true", ""); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, this.destPath); + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, this.destPath, respmap.copy()); } break; default: try { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, new String(resp.body().bytes(), "UTF-8")); + invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_UTF8, new String(resp.body().bytes(), "UTF-8"), respmap.copy()); } catch (IOException e) { - callback.invoke("RNFetchBlob failed to encode response data to UTF8 string.", null); + invoke_callback("ReactNativeBlobUtil failed to encode response data to UTF8 string.", respmap.copy()); } break; } @@ -639,27 +810,30 @@ private void done(Response resp) { /** * Invoke this method to enable download progress reporting. + * * @param taskId Task ID of the HTTP task. * @return Task ID of the target task */ - public static RNFetchBlobProgressConfig getReportProgress(String taskId) { - if(!progressReport.containsKey(taskId)) return null; + public static ReactNativeBlobUtilProgressConfig getReportProgress(String taskId) { + if (!progressReport.containsKey(taskId)) return null; return progressReport.get(taskId); } /** * Invoke this method to enable download progress reporting. + * * @param taskId Task ID of the HTTP task. * @return Task ID of the target task */ - public static RNFetchBlobProgressConfig getReportUploadProgress(String taskId) { - if(!uploadProgressReport.containsKey(taskId)) return null; + public static ReactNativeBlobUtilProgressConfig getReportUploadProgress(String taskId) { + if (!uploadProgressReport.containsKey(taskId)) return null; return uploadProgressReport.get(taskId); } /** * Create response information object, contains status code, headers, etc. - * @param resp Response object + * + * @param resp Response object * @param isBlobResp If the response is binary data * @return Get RCT bridge object contains response information. */ @@ -670,26 +844,23 @@ private WritableMap getResponseInfo(Response resp, boolean isBlobResp) { info.putString("taskId", this.taskId); info.putBoolean("timeout", timeout); WritableMap headers = Arguments.createMap(); - for(int i =0;i< resp.headers().size();i++) { + for (int i = 0; i < resp.headers().size(); i++) { headers.putString(resp.headers().name(i), resp.headers().value(i)); } WritableArray redirectList = Arguments.createArray(); - for(String r : redirects) { + for (String r : redirects) { redirectList.pushString(r); } info.putArray("redirects", redirectList); info.putMap("headers", headers); Headers h = resp.headers(); - if(isBlobResp) { + if (isBlobResp) { info.putString("respType", "blob"); - } - else if(getHeaderIgnoreCases(h, "content-type").equalsIgnoreCase("text/")) { + } else if (getHeaderIgnoreCases(h, "content-type").equalsIgnoreCase("text/")) { info.putString("respType", "text"); - } - else if(getHeaderIgnoreCases(h, "content-type").contains("application/json")) { + } else if (getHeaderIgnoreCases(h, "content-type").contains("application/json")) { info.putString("respType", "json"); - } - else { + } else { info.putString("respType", ""); } return info; @@ -697,6 +868,7 @@ else if(getHeaderIgnoreCases(h, "content-type").contains("application/json")) { /** * Check if response data is binary data. + * * @param resp OkHttp response. * @return If the response data contains binary bytes */ @@ -706,40 +878,40 @@ private boolean isBlobResponse(Response resp) { boolean isText = !ctype.equalsIgnoreCase("text/"); boolean isJSON = !ctype.equalsIgnoreCase("application/json"); boolean isCustomBinary = false; - if(options.binaryContentTypes != null) { - for(int i = 0; i< options.binaryContentTypes.size();i++) { - if(ctype.toLowerCase().contains(options.binaryContentTypes.getString(i).toLowerCase())) { + if (options.binaryContentTypes != null) { + for (int i = 0; i < options.binaryContentTypes.size(); i++) { + if (ctype.toLowerCase(Locale.ROOT).contains(options.binaryContentTypes.getString(i).toLowerCase(Locale.ROOT))) { isCustomBinary = true; break; } } } - return (!(isJSON || isText)) || isCustomBinary; + return (!(isJSON || isText)) || isCustomBinary; } private String getHeaderIgnoreCases(Headers headers, String field) { String val = headers.get(field); - if(val != null) return val; - return headers.get(field.toLowerCase()) == null ? "" : headers.get(field.toLowerCase()); + if (val != null) return val; + return headers.get(field.toLowerCase(Locale.ROOT)) == null ? "" : headers.get(field.toLowerCase(Locale.ROOT)); } - private String getHeaderIgnoreCases(HashMap headers, String field) { + private String getHeaderIgnoreCases(HashMap headers, String field) { String val = headers.get(field); - if(val != null) return val; - String lowerCasedValue = headers.get(field.toLowerCase()); + if (val != null) return val; + String lowerCasedValue = headers.get(field.toLowerCase(Locale.ROOT)); return lowerCasedValue == null ? "" : lowerCasedValue; } private void emitStateEvent(WritableMap args) { - RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_HTTP_STATE, args); + ReactNativeBlobUtilImpl.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(ReactNativeBlobUtilConst.EVENT_HTTP_STATE, args); } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { - Context appCtx = RNFetchBlob.RCTContext.getApplicationContext(); + Context appCtx = ReactNativeBlobUtilImpl.RCTContext.getApplicationContext(); long id = intent.getExtras().getLong(DownloadManager.EXTRA_DOWNLOAD_ID); if (id == this.downloadManagerId) { releaseTaskResource(); // remove task ID from task map @@ -749,9 +921,8 @@ public void onReceive(Context context, Intent intent) { DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE); dm.query(query); Cursor c = dm.query(query); - // #236 unhandled null check for DownloadManager.query() return value if (c == null) { - this.callback.invoke("Download manager failed to download from " + this.url + ". Query was unsuccessful ", null, null); + this.invoke_callback("Download manager failed to download from " + this.url + ". Query was unsuccessful ", null, null); return; } @@ -759,18 +930,15 @@ public void onReceive(Context context, Intent intent) { try { // the file exists in media content database if (c.moveToFirst()) { - // #297 handle failed request int statusCode = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)); - if(statusCode == DownloadManager.STATUS_FAILED) { - this.callback.invoke("Download manager failed to download from " + this.url + ". Status Code = " + statusCode, null, null); + if (statusCode == DownloadManager.STATUS_FAILED) { + this.invoke_callback("Download manager failed to download from " + this.url + ". Status Code = " + statusCode, null, null); return; } String contentUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); - if ( contentUri != null && - options.addAndroidDownloads.hasKey("mime") && - options.addAndroidDownloads.getString("mime").contains("image")) { + if (contentUri != null) { Uri uri = Uri.parse(contentUri); - Cursor cursor = appCtx.getContentResolver().query(uri, new String[]{android.provider.MediaStore.Images.ImageColumns.DATA}, null, null, null); + Cursor cursor = appCtx.getContentResolver().query(uri, new String[]{android.provider.MediaStore.Files.FileColumns.DATA}, null, null, null); // use default destination of DownloadManager if (cursor != null) { cursor.moveToFirst(); @@ -786,25 +954,32 @@ public void onReceive(Context context, Intent intent) { } // When the file is not found in media content database, check if custom path exists - if (options.addAndroidDownloads.hasKey("path")) { + if (options.addAndroidDownloads.hasKey("path") || options.addAndroidDownloads.hasKey("storeLocal")) { try { - String customDest = options.addAndroidDownloads.getString("path"); + String customDest = customPath; boolean exists = new File(customDest).exists(); - if(!exists) + if (!exists) throw new Exception("Download manager download failed, the file does not downloaded to destination."); else - this.callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, customDest); + this.invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, customDest); - } catch(Exception ex) { + } catch (Exception ex) { ex.printStackTrace(); - this.callback.invoke(ex.getLocalizedMessage(), null); + this.invoke_callback(ex.getLocalizedMessage(), null); } } + else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.addAndroidDownloads.hasKey("storeInDownloads") && options.addAndroidDownloads.getBoolean("storeInDownloads")){ + Uri downloadeduri = dm.getUriForDownloadedFile(downloadManagerId); + if(downloadeduri == null) + this.invoke_callback("Download manager could not resolve downloaded file uri.", ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, null); + else + this.invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, downloadeduri.toString()); + } else { - if(filePath == null) - this.callback.invoke("Download manager could not resolve downloaded file path.", RNFetchBlobConst.RNFB_RESPONSE_PATH, null); + if (filePath == null) + this.invoke_callback("Download manager could not resolve downloaded file path.", ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, null); else - this.callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, filePath); + this.invoke_callback(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, filePath); } } @@ -814,37 +989,26 @@ public void onReceive(Context context, Intent intent) { public static OkHttpClient.Builder enableTls12OnPreLollipop(OkHttpClient.Builder client) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { try { - // Code from https://stackoverflow.com/a/40874952/544779 - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); - } - X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, new TrustManager[] { trustManager }, null); - SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - - client.sslSocketFactory(sslSocketFactory, trustManager); + // Code from https://github.com/square/okhttp/issues/2372#issuecomment-244807676 + SSLContext sc = SSLContext.getInstance("TLSv1.2"); + sc.init(null, null, null); + client.sslSocketFactory(new Tls12SocketFactory(sc.getSocketFactory())); ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) .build(); - List< ConnectionSpec > specs = new ArrayList < > (); + List specs = new ArrayList<>(); specs.add(cs); specs.add(ConnectionSpec.COMPATIBLE_TLS); specs.add(ConnectionSpec.CLEARTEXT); client.connectionSpecs(specs); } catch (Exception exc) { - FLog.e("OkHttpClientProvider", "Error while enabling TLS 1.2", exc); + FLog.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc); } } return client; } - - } diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilStream.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilStream.java new file mode 100644 index 000000000..b958a952d --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilStream.java @@ -0,0 +1,290 @@ +package com.ReactNativeBlobUtil; + +import static com.ReactNativeBlobUtil.ReactNativeBlobUtilConst.EVENT_FILESYSTEM; + +import android.net.Uri; +import android.os.SystemClock; +import android.util.Base64; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.UUID; + +public class ReactNativeBlobUtilStream { + private final DeviceEventManagerModule.RCTDeviceEventEmitter emitter; + private String encoding = "base64"; + private OutputStream writeStreamInstance = null; + private static final HashMap fileStreams = new HashMap<>(); + + ReactNativeBlobUtilStream(ReactApplicationContext ctx) { + this.emitter = ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + } + + /** + * Create a file stream for read + * @param path File stream target path + * @param encoding File stream decoder, should be one of `base64`, `utf8`, `ascii` + * @param bufferSize Buffer size of read stream, default to 4096 (4095 when encode is `base64`) + * @param RCTContext + */ + void readStream(String path, String encoding, int bufferSize, int tick, final String streamId, ReactApplicationContext RCTContext) { + String resolved = ReactNativeBlobUtilUtils.normalizePath(path); + if (resolved != null) + path = resolved; + + try { + int chunkSize = encoding.equalsIgnoreCase("base64") ? 4095 : 4096; + if (bufferSize > 0) + chunkSize = bufferSize; + + InputStream fs; + + if (resolved != null && path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET)) { + fs = ReactNativeBlobUtilImpl.RCTContext.getAssets().open(path.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, "")); + } + // fix issue 287 + else if (resolved == null) { + fs = ReactNativeBlobUtilImpl.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); + } else { + fs = new FileInputStream(new File(path)); + } + + int cursor = 0; + boolean error = false; + + if (encoding.equalsIgnoreCase("utf8")) { + InputStreamReader isr = new InputStreamReader(fs, Charset.forName("UTF-8")); + BufferedReader reader = new BufferedReader(isr, chunkSize); + char[] buffer = new char[chunkSize]; + int numBytesRead; + // read chunks of the string + while ((numBytesRead = reader.read(buffer, 0, chunkSize)) != -1) { + String chunk = new String(buffer, 0, numBytesRead); + emitStreamEvent(streamId, "data", chunk); + if (tick > 0) + SystemClock.sleep(tick); + } + + reader.close(); + isr.close(); + } else if (encoding.equalsIgnoreCase("ascii")) { + byte[] buffer = new byte[chunkSize]; + while ((cursor = fs.read(buffer)) != -1) { + WritableArray chunk = Arguments.createArray(); + for (int i = 0; i < cursor; i++) { + chunk.pushInt((int) buffer[i]); + } + emitStreamEvent(streamId, "data", chunk); + if (tick > 0) + SystemClock.sleep(tick); + } + } else if (encoding.equalsIgnoreCase("base64")) { + byte[] buffer = new byte[chunkSize]; + while ((cursor = fs.read(buffer)) != -1) { + if (cursor < chunkSize) { + byte[] copy = new byte[cursor]; + System.arraycopy(buffer, 0, copy, 0, cursor); + emitStreamEvent(streamId, "data", Base64.encodeToString(copy, Base64.NO_WRAP)); + } else + emitStreamEvent(streamId, "data", Base64.encodeToString(buffer, Base64.NO_WRAP)); + if (tick > 0) + SystemClock.sleep(tick); + } + } else { + emitStreamEvent( + streamId, + "error", + "EINVAL", + "Unrecognized encoding `" + encoding + "`, should be one of `base64`, `utf8`, `ascii`" + ); + error = true; + } + + if (!error) + emitStreamEvent(streamId, "end", ""); + fs.close(); + + } catch (FileNotFoundException err) { + emitStreamEvent( + streamId, + "error", + "ENOENT", + "No such file '" + path + "'" + ); + } catch (Exception err) { + emitStreamEvent( + streamId, + "error", + "EUNSPECIFIED", + "Failed to convert data to " + encoding + " encoded string. This might be because this encoding cannot be used for this data." + ); + err.printStackTrace(); + } + } + + /** + * Create a write stream and store its instance in ReactNativeBlobUtilFS.fileStreams + * + * @param path Target file path + * @param encoding Should be one of `base64`, `utf8`, `ascii` + * @param append Flag represents if the file stream overwrite existing content + * @param callback Callback + */ + void writeStream(String path, String encoding, boolean append, Callback callback) { + String resolved = ReactNativeBlobUtilUtils.normalizePath(path); + if (resolved != null) + path = resolved; + + try { + File dest = new File(path); + File dir = dest.getParentFile(); + + if (resolved != null && !dest.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs()) { + callback.invoke("ENOTDIR", "Failed to create parent directory of '" + path + "'"); + return; + } + } + if (!dest.createNewFile()) { + callback.invoke("ENOENT", "File '" + path + "' does not exist and could not be created"); + return; + } + } else if (dest.isDirectory()) { + callback.invoke("EISDIR", "Expecting a file but '" + path + "' is a directory"); + return; + } + + OutputStream fs; + if (resolved != null && path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET)) { + fs = ReactNativeBlobUtilImpl.RCTContext.getAssets().openFd(path.replace(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET, "")).createOutputStream (); + } + // fix issue 287 + else if (resolved == null) { + fs = ReactNativeBlobUtilImpl.RCTContext.getContentResolver().openOutputStream(Uri.parse(path)); + } else { + fs = new FileOutputStream(path, append); + } + this.encoding = encoding; + String streamId = UUID.randomUUID().toString(); + ReactNativeBlobUtilStream.fileStreams.put(streamId, this); + this.writeStreamInstance = fs; + callback.invoke(null, null, streamId); + } catch (Exception err) { + callback.invoke("EUNSPECIFIED", "Failed to create write stream at path `" + path + "`; " + err.getLocalizedMessage()); + } + } + + /** + * Write a chunk of data into a file stream. + * + * @param streamId File stream ID + * @param data Data chunk in string format + * @param callback JS context callback + */ + static void writeChunk(String streamId, String data, Callback callback) { + ReactNativeBlobUtilStream fs = fileStreams.get(streamId); + assert fs != null; + OutputStream stream = fs.writeStreamInstance; + byte[] chunk = ReactNativeBlobUtilUtils.stringToBytes(data, fs.encoding); + try { + stream.write(chunk); + callback.invoke(); + } catch (Exception e) { + callback.invoke(e.getLocalizedMessage()); + } + } + + /** + * Write data using ascii array + * + * @param streamId File stream ID + * @param data Data chunk in ascii array format + * @param callback JS context callback + */ + static void writeArrayChunk(String streamId, ReadableArray data, Callback callback) { + try { + ReactNativeBlobUtilStream fs = fileStreams.get(streamId); + assert fs != null; + OutputStream stream = fs.writeStreamInstance; + byte[] chunk = new byte[data.size()]; + for (int i = 0; i < data.size(); i++) { + chunk[i] = (byte) data.getInt(i); + } + stream.write(chunk); + callback.invoke(); + } catch (Exception e) { + callback.invoke(e.getLocalizedMessage()); + } + } + + /** + * Close file write stream by ID + * + * @param streamId Stream ID + * @param callback JS context callback + */ + static void closeStream(String streamId, Callback callback) { + try { + ReactNativeBlobUtilStream fs = fileStreams.get(streamId); + assert fs != null; + OutputStream stream = fs.writeStreamInstance; + fileStreams.remove(streamId); + stream.close(); + callback.invoke(); + } catch (Exception err) { + callback.invoke(err.getLocalizedMessage()); + } + } + + /** + * Private method for emit read stream event. + * + * @param streamName ID of the read stream + * @param event Event name, `data`, `end`, `error`, etc. + * @param data Event data + */ + private void emitStreamEvent(String streamName, String event, String data) { + WritableMap eventData = Arguments.createMap(); + eventData.putString("event", event); + eventData.putString("detail", data); + eventData.putString("streamId", streamName); + this.emitter.emit(EVENT_FILESYSTEM, eventData); + } + + // "event" always is "data"... + private void emitStreamEvent(String streamName, String event, WritableArray data) { + WritableMap eventData = Arguments.createMap(); + eventData.putString("event", event); + eventData.putArray("detail", data); + eventData.putString("streamId", streamName); + this.emitter.emit(EVENT_FILESYSTEM, eventData); + } + + // "event" always is "error"... + private void emitStreamEvent(String streamName, String event, String code, String message) { + WritableMap eventData = Arguments.createMap(); + eventData.putString("event", event); + eventData.putString("code", code); + eventData.putString("detail", message); + eventData.putString("streamId", streamName); + this.emitter.emit(EVENT_FILESYSTEM, eventData); + } +} diff --git a/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilUtils.java b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilUtils.java new file mode 100644 index 000000000..1565e510d --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilUtils.java @@ -0,0 +1,138 @@ +package com.ReactNativeBlobUtil; + +import android.net.Uri; +import android.util.Base64; + +import com.ReactNativeBlobUtil.Utils.PathResolver; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.util.Locale; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import okhttp3.OkHttpClient; + +public class ReactNativeBlobUtilUtils { + + public static X509TrustManager sharedTrustManager; + + public static String getMD5(String input) { + String result = null; + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(input.getBytes()); + byte[] digest = md.digest(); + + StringBuilder sb = new StringBuilder(); + + for (byte b : digest) { + sb.append(String.format(Locale.ROOT, "%02x", b & 0xff)); + } + + result = sb.toString(); + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + // TODO: Is discarding errors the intent? (https://www.owasp.org/index.php/Return_Inside_Finally_Block) + return result; + } + + } + + public static void emitWarningEvent(String data) { + WritableMap args = Arguments.createMap(); + args.putString("event", "warn"); + args.putString("detail", data); + + // emit event to js context + ReactNativeBlobUtilImpl.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(ReactNativeBlobUtilConst.EVENT_MESSAGE, args); + } + + public static OkHttpClient.Builder getUnsafeOkHttpClient(OkHttpClient client) { + try { + + if (sharedTrustManager == null) throw new IllegalStateException("Use of own trust manager but none defined"); + + final TrustManager[] trustAllCerts = new TrustManager[]{sharedTrustManager}; + + // Install the all-trusting trust manager + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + // Create an ssl socLket factory with our all-trusting manager + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + OkHttpClient.Builder builder = client.newBuilder(); + builder.sslSocketFactory(sslSocketFactory, sharedTrustManager); + builder.hostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + + return builder; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * String to byte converter method + * + * @param data Raw data in string format + * @param encoding Decoder name + * @return Converted data byte array + */ + public static byte[] stringToBytes(String data, String encoding) { + if (encoding.equalsIgnoreCase("ascii")) { + return data.getBytes(Charset.forName("US-ASCII")); + } else if (encoding.toLowerCase(Locale.ROOT).contains("base64")) { + return Base64.decode(data, Base64.NO_WRAP); + + } else if (encoding.equalsIgnoreCase("utf8")) { + return data.getBytes(Charset.forName("UTF-8")); + } + return data.getBytes(Charset.forName("US-ASCII")); + } + + /** + * Normalize the path, remove URI scheme (xxx://) so that we can handle it. + * + * @param path URI string. + * @return Normalized string + */ + public static String normalizePath(String path) { + if (path == null) + return null; + if (!path.matches("\\w+\\:.*")) + return path; + if (path.startsWith("file://")) { + return path.replace("file://", ""); + } + + Uri uri = Uri.parse(path); + if (path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET)) { + return path; + } else + return PathResolver.getRealPathFromURI(ReactNativeBlobUtilImpl.RCTContext, uri); + } + + public static boolean isAsset(String path) { + return path != null && path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_BUNDLE_ASSET); + } + + public static boolean isContentUri(String path) { + return path != null && path.startsWith(ReactNativeBlobUtilConst.FILE_PREFIX_CONTENT); + } +} diff --git a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobDefaultResp.java b/android/src/main/java/com/ReactNativeBlobUtil/Response/ReactNativeBlobUtilDefaultResp.java similarity index 72% rename from android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobDefaultResp.java rename to android/src/main/java/com/ReactNativeBlobUtil/Response/ReactNativeBlobUtilDefaultResp.java index 3982a8b56..f15493dd9 100644 --- a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobDefaultResp.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/Response/ReactNativeBlobUtilDefaultResp.java @@ -1,8 +1,8 @@ -package com.RNFetchBlob.Response; +package com.ReactNativeBlobUtil.Response; -import com.RNFetchBlob.RNFetchBlobConst; -import com.RNFetchBlob.RNFetchBlobProgressConfig; -import com.RNFetchBlob.RNFetchBlobReq; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilConst; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilProgressConfig; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilReq; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; @@ -22,14 +22,14 @@ /** * Created by wkh237 on 2016/7/11. */ -public class RNFetchBlobDefaultResp extends ResponseBody { +public class ReactNativeBlobUtilDefaultResp extends ResponseBody { String mTaskId; ReactApplicationContext rctContext; ResponseBody originalBody; boolean isIncrement = false; - public RNFetchBlobDefaultResp(ReactApplicationContext ctx, String taskId, ResponseBody body, boolean isIncrement) { + public ReactNativeBlobUtilDefaultResp(ReactApplicationContext ctx, String taskId, ResponseBody body, boolean isIncrement) { this.rctContext = ctx; this.mTaskId = taskId; this.originalBody = body; @@ -63,24 +63,23 @@ private class ProgressReportingSource implements Source { @Override public long read(Buffer sink, long byteCount) throws IOException { - long read = mOriginalSource.read(sink, byteCount); + long read = mOriginalSource.read(sink, byteCount); bytesRead += read > 0 ? read : 0; - RNFetchBlobProgressConfig reportConfig = RNFetchBlobReq.getReportProgress(mTaskId); + ReactNativeBlobUtilProgressConfig reportConfig = ReactNativeBlobUtilReq.getReportProgress(mTaskId); long cLen = contentLength(); - if(reportConfig != null && cLen != 0 && reportConfig.shouldReport(bytesRead/contentLength())) { + if (reportConfig != null && cLen != 0 && reportConfig.shouldReport(bytesRead / contentLength())) { WritableMap args = Arguments.createMap(); args.putString("taskId", mTaskId); args.putString("written", String.valueOf(bytesRead)); args.putString("total", String.valueOf(contentLength())); - if(isIncrement) { + if (isIncrement) { args.putString("chunk", sink.readString(Charset.defaultCharset())); - } - else { + } else { args.putString("chunk", ""); } rctContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_PROGRESS, args); + .emit(ReactNativeBlobUtilConst.EVENT_PROGRESS, args); } return read; } diff --git a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java b/android/src/main/java/com/ReactNativeBlobUtil/Response/ReactNativeBlobUtilFileResp.java similarity index 78% rename from android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java rename to android/src/main/java/com/ReactNativeBlobUtil/Response/ReactNativeBlobUtilFileResp.java index 2470eef61..81bbd0852 100644 --- a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/Response/ReactNativeBlobUtilFileResp.java @@ -1,10 +1,10 @@ -package com.RNFetchBlob.Response; +package com.ReactNativeBlobUtil.Response; import androidx.annotation.NonNull; -import com.RNFetchBlob.RNFetchBlobConst; -import com.RNFetchBlob.RNFetchBlobProgressConfig; -import com.RNFetchBlob.RNFetchBlobReq; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilConst; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilProgressConfig; +import com.ReactNativeBlobUtil.ReactNativeBlobUtilReq; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; @@ -25,7 +25,7 @@ /** * Created by wkh237 on 2016/7/11. */ -public class RNFetchBlobFileResp extends ResponseBody { +public class ReactNativeBlobUtilFileResp extends ResponseBody { String mTaskId; ResponseBody originalBody; @@ -35,7 +35,12 @@ public class RNFetchBlobFileResp extends ResponseBody { FileOutputStream ofStream; boolean isEndMarkerReceived; - public RNFetchBlobFileResp(ReactApplicationContext ctx, String taskId, ResponseBody body, String path, boolean overwrite) throws IOException { + public ReactNativeBlobUtilFileResp(ResponseBody body) { + super(); + this.originalBody = body; + } + + public ReactNativeBlobUtilFileResp(ReactApplicationContext ctx, String taskId, ResponseBody body, String path, boolean overwrite) throws IOException { super(); this.rctContext = ctx; this.mTaskId = taskId; @@ -50,11 +55,11 @@ public RNFetchBlobFileResp(ReactApplicationContext ctx, String taskId, ResponseB File f = new File(path); File parent = f.getParentFile(); - if(parent != null && !parent.exists() && !parent.mkdirs()){ + if (parent != null && !parent.exists() && !parent.mkdirs()) { throw new IllegalStateException("Couldn't create dir: " + parent); } - if(!f.exists()) + if (!f.exists()) f.createNewFile(); ofStream = new FileOutputStream(new File(path), appendToExistingFile); } @@ -67,6 +72,10 @@ public MediaType contentType() { @Override public long contentLength() { + if (originalBody.contentLength() > Integer.MAX_VALUE) { + // This is a workaround for a bug Okio buffer where it can't handle larger than int. + return Integer.MAX_VALUE; + } return originalBody.contentLength(); } @@ -94,13 +103,13 @@ public long read(@NonNull Buffer sink, long byteCount) throws IOException { // End marker has been received for chunked download isEndMarkerReceived = true; } - RNFetchBlobProgressConfig reportConfig = RNFetchBlobReq.getReportProgress(mTaskId); + ReactNativeBlobUtilProgressConfig reportConfig = ReactNativeBlobUtilReq.getReportProgress(mTaskId); if (contentLength() != 0) { // For non-chunked download, progress is received / total // For chunked download, progress can be either 0 (started) or 1 (ended) - float progress = (contentLength() != -1) ? bytesDownloaded / contentLength() : ( ( isEndMarkerReceived ) ? 1 : 0 ); + float progress = (contentLength() != -1) ? bytesDownloaded / contentLength() : ((isEndMarkerReceived) ? 1 : 0); if (reportConfig != null && reportConfig.shouldReport(progress /* progress */)) { if (contentLength() != -1) { @@ -110,7 +119,7 @@ public long read(@NonNull Buffer sink, long byteCount) throws IOException { // For chunked downloads if (!isEndMarkerReceived) { reportProgress(mTaskId, 0, contentLength()); - } else{ + } else { reportProgress(mTaskId, bytesDownloaded, bytesDownloaded); } } @@ -119,7 +128,7 @@ public long read(@NonNull Buffer sink, long byteCount) throws IOException { } return read; - } catch(Exception ex) { + } catch (Exception ex) { return -1; } } @@ -130,7 +139,7 @@ private void reportProgress(String taskId, long bytesDownloaded, long contentLen args.putString("written", String.valueOf(bytesDownloaded)); args.putString("total", String.valueOf(contentLength)); rctContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_PROGRESS, args); + .emit(ReactNativeBlobUtilConst.EVENT_PROGRESS, args); } @Override diff --git a/android/src/main/java/com/ReactNativeBlobUtil/Utils/FileDescription.java b/android/src/main/java/com/ReactNativeBlobUtil/Utils/FileDescription.java new file mode 100644 index 000000000..9226f30fa --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/Utils/FileDescription.java @@ -0,0 +1,19 @@ +package com.ReactNativeBlobUtil.Utils; + +import android.webkit.MimeTypeMap; + +public class FileDescription { + public String name; + public String partentFolder; + public String mimeType; + + public FileDescription(String n, String mT, String pF) { + name = n; + partentFolder = pF != null ? pF : ""; + mimeType = mT; + } + + public String getFullPath(){ + return partentFolder + "/" + MimeType.getFullFileName(name, mimeType); + } +} diff --git a/android/src/main/java/com/RNFetchBlob/Utils/FileProvider.java b/android/src/main/java/com/ReactNativeBlobUtil/Utils/FileProvider.java similarity index 65% rename from android/src/main/java/com/RNFetchBlob/Utils/FileProvider.java rename to android/src/main/java/com/ReactNativeBlobUtil/Utils/FileProvider.java index d48f75fb1..34ce66e00 100644 --- a/android/src/main/java/com/RNFetchBlob/Utils/FileProvider.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/Utils/FileProvider.java @@ -1,4 +1,4 @@ -package com.RNFetchBlob.Utils; +package com.ReactNativeBlobUtil.Utils; public class FileProvider extends androidx.core.content.FileProvider { } diff --git a/android/src/main/java/com/ReactNativeBlobUtil/Utils/MimeType.java b/android/src/main/java/com/ReactNativeBlobUtil/Utils/MimeType.java new file mode 100644 index 000000000..061bebcd0 --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/Utils/MimeType.java @@ -0,0 +1,70 @@ +package com.ReactNativeBlobUtil.Utils; + +import android.webkit.MimeTypeMap; + +import org.apache.commons.lang3.StringUtils; + +public class MimeType { + static String UNKNOWN = "*/*"; + static String BINARY_FILE = "application/octet-stream"; + static String IMAGE = "image/*"; + static String AUDIO = "audio/*"; + static String VIDEO = "video/*"; + static String TEXT = "text/*"; + static String FONT = "font/*"; + static String APPLICATION = "application/*"; + static String CHEMICAL = "chemical/*"; + static String MODEL = "model/*"; + + /** + * * Given `name` = `ABC` AND `mimeType` = `video/mp4`, then return `ABC.mp4` + * * Given `name` = `ABC` AND `mimeType` = `null`, then return `ABC` + * * Given `name` = `ABC.mp4` AND `mimeType` = `video/mp4`, then return `ABC.mp4` + * + * @param name can have file extension or not + */ + + public static String getFullFileName(String name, String mimeType) { + // Prior to API 29, MimeType.BINARY_FILE has no file extension + String ext = MimeType.getExtensionFromMimeType(mimeType); + if ((ext == null || ext.isEmpty()) || name.endsWith("." + ext)) return name; + else { + String fn = name + "." + ext; + if (fn.endsWith(".")) return StringUtils.stripEnd(fn, "."); + else return fn; + } + } + + /** + * Some mime types return no file extension on older API levels. This function adds compatibility accross API levels. + * + * @see this.getExtensionFromMimeTypeOrFileName + */ + + public static String getExtensionFromMimeType(String mimeType) { + if (mimeType != null) { + if (mimeType.equals(BINARY_FILE)) return "bin"; + else return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + } else return ""; + } + + /** + * @see this.getExtensionFromMimeType + */ + public static String getExtensionFromMimeTypeOrFileName(String mimeType, String filename) { + if (mimeType == null || mimeType.equals(UNKNOWN)) return StringUtils.substringAfterLast(filename, "."); + else return getExtensionFromMimeType(mimeType); + } + + /** + * Some file types return no mime type on older API levels. This function adds compatibility across API levels. + */ + public static String getMimeTypeFromExtension(String fileExtension) { + if (fileExtension.equals("bin")) return BINARY_FILE; + else { + String mt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + if (mt != null) return mt; + else return UNKNOWN; + } + } +} diff --git a/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java b/android/src/main/java/com/ReactNativeBlobUtil/Utils/PathResolver.java similarity index 86% rename from android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java rename to android/src/main/java/com/ReactNativeBlobUtil/Utils/PathResolver.java index c83fdbaf6..511189913 100644 --- a/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java +++ b/android/src/main/java/com/ReactNativeBlobUtil/Utils/PathResolver.java @@ -1,4 +1,4 @@ -package com.RNFetchBlob.Utils; +package com.ReactNativeBlobUtil.Utils; import android.annotation.TargetApi; import android.content.Context; @@ -8,9 +8,10 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.content.ContentUris; -import android.os.Environment; import android.content.ContentResolver; -import com.RNFetchBlob.RNFetchBlobUtils; + +import com.ReactNativeBlobUtil.ReactNativeBlobUtilUtils; + import java.io.File; import java.io.InputStream; import java.io.FileOutputStream; @@ -30,7 +31,9 @@ public static String getRealPathFromURI(final Context context, final Uri uri) { final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; + File dir = context.getExternalFilesDir(null); + if (dir != null) return dir + "/" + split[1]; + return ""; } // TODO handle non-primary volumes @@ -46,16 +49,24 @@ else if (isDownloadsDocument(uri)) { String path = rawuri.getPath(); return path; } - final Uri contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + Long docId = null; + //Since Android 10, uri can start with msf scheme like "msf:12345" + if (id != null && id.startsWith("msf:")) { + final String[] split = id.split(":"); + docId = Long.valueOf(split[1]); + } else { + docId = Long.valueOf(id); + } + + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), docId); return getDataColumn(context, contentUri, null, null); - } - catch (Exception ex) { + } catch (Exception ex) { //something went wrong, but android should still be able to handle the original uri by returning null here (see readFile(...)) return null; } - + } // MediaProvider else if (isMediaDocument(uri)) { @@ -73,13 +84,12 @@ else if (isMediaDocument(uri)) { } final String selection = "_id=?"; - final String[] selectionArgs = new String[] { + final String[] selectionArgs = new String[]{ split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); - } - else if ("content".equalsIgnoreCase(uri.getScheme())) { + } else if ("content".equalsIgnoreCase(uri.getScheme())) { // Return the remote address if (isGooglePhotosUri(uri)) @@ -88,7 +98,7 @@ else if ("content".equalsIgnoreCase(uri.getScheme())) { return getDataColumn(context, uri, null, null); } // Other Providers - else{ + else { try { InputStream attachment = context.getContentResolver().openInputStream(uri); if (attachment != null) { @@ -106,7 +116,7 @@ else if ("content".equalsIgnoreCase(uri.getScheme())) { } } } catch (Exception e) { - RNFetchBlobUtils.emitWarningEvent(e.toString()); + ReactNativeBlobUtilUtils.emitWarningEvent(e.toString()); return null; } } @@ -144,9 +154,9 @@ private static String getContentName(ContentResolver resolver, Uri uri) { * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * - * @param context The context. - * @param uri The Uri to query. - * @param selection (Optional) Filter used in the query. + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ @@ -167,12 +177,10 @@ public static String getDataColumn(Context context, Uri uri, String selection, final int index = cursor.getColumnIndexOrThrow(column); result = cursor.getString(index); } - } - catch (Exception ex) { + } catch (Exception ex) { ex.printStackTrace(); return null; - } - finally { + } finally { if (cursor != null) cursor.close(); } @@ -212,4 +220,4 @@ public static boolean isGooglePhotosUri(Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/ReactNativeBlobUtil/Utils/Tls12SocketFactory.java b/android/src/main/java/com/ReactNativeBlobUtil/Utils/Tls12SocketFactory.java new file mode 100644 index 000000000..2343ce889 --- /dev/null +++ b/android/src/main/java/com/ReactNativeBlobUtil/Utils/Tls12SocketFactory.java @@ -0,0 +1,69 @@ +package com.ReactNativeBlobUtil.Utils; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * Enables TLS v1.2 when creating SSLSockets. + * For some reason, android supports TLS v1.2 from API 16, but enables it by + * default only from API 20. + * + * @see Reference on SSLSocket + * @see SSLSocketFactory + */ +public class Tls12SocketFactory extends SSLSocketFactory { + private static final String[] TLS_V12_ONLY = {"TLSv1.2"}; + + final SSLSocketFactory delegate; + + public Tls12SocketFactory(SSLSocketFactory base) { + this.delegate = base; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return patch(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return patch(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return patch(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return patch(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return patch(delegate.createSocket(address, port, localAddress, localPort)); + } + + private Socket patch(Socket s) { + if (s instanceof SSLSocket) { + ((SSLSocket) s).setEnabledProtocols(TLS_V12_ONLY); + } + return s; + } +} \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index de5cc9fd5..b355de200 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - rn-fetch-blob + react-native-blob-util diff --git a/android/src/newarch/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java b/android/src/newarch/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java new file mode 100644 index 000000000..4e753dd4d --- /dev/null +++ b/android/src/newarch/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java @@ -0,0 +1,293 @@ +package com.ReactNativeBlobUtil; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.facebook.fbreact.specs.NativeBlobUtilsSpec; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReactMethod; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + +public class ReactNativeBlobUtil extends NativeBlobUtilsSpec { + + private final ReactNativeBlobUtilImpl delegate; + + public ReactNativeBlobUtil(ReactApplicationContext reactContext) { + super(reactContext); + delegate = new ReactNativeBlobUtilImpl(reactContext); + } + + // Required for rn built in EventEmitter Calls. + @ReactMethod + public void addListener(String eventName) { + + } + + @ReactMethod + public void removeListeners(double count) { + + } + + @Override + protected Map getTypedExportedConstants() { + Map res = new HashMap<>(); + res.putAll(ReactNativeBlobUtilFS.getSystemfolders(this.getReactApplicationContext())); + res.putAll(ReactNativeBlobUtilFS.getLegacySystemfolders(this.getReactApplicationContext())); + return res; + } + + @NonNull + @Override + public String getName() { + return ReactNativeBlobUtilImpl.NAME; + } + + @Override + public void fetchBlobForm(ReadableMap options, String taskId, String method, String url, ReadableMap headers, ReadableArray form, Callback callback) { + delegate.fetchBlobForm(options, taskId, method, url, headers, form, callback); + } + + @Override + public void fetchBlob(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, Callback callback) { + delegate.fetchBlob(options, taskId, method, url, headers, body, callback); + } + + @Override + public void createFile(String path, String data, String encoding, Promise promise) { + delegate.createFile(path, data, encoding, promise); + } + + @Override + public void createFileASCII(String path, ReadableArray data, Promise promise) { + delegate.createFileASCII(path, data, promise); + } + + + @Override + public void pathForAppGroup(String groupName, Promise promise) { + // Not implemented as ReactNativeBlobUtil.pathForAppGroup only supports IOS + // This will be rejected at the iOS layer + } + + @Override + public String syncPathAppGroup(String groupName) { + // Not implemented as ReactNativeBlobUtil.syncPathAppGroup only supports IOS + // This will be rejected at the iOS layer + return null; + } + + @Override + public void exists(String path, Callback callback) { + delegate.exists(path, callback); + } + + @Override + public void writeFile(String path, String encoding, String data, boolean transformFile, boolean append, Promise promise) { + delegate.writeFile(path, encoding, data, transformFile, append, promise); + } + + @Override + public void writeFileArray(String path, ReadableArray data, boolean append, Promise promise) { + delegate.writeFileArray(path, data, append, promise); + } + + @Override + public void writeStream(String path, String withEncoding, boolean appendData, Callback callback) { + delegate.writeStream(path, withEncoding, appendData, callback); + } + + @Override + public void writeArrayChunk(String streamId, ReadableArray withArray, Callback callback) { + delegate.writeArrayChunk(streamId, withArray, callback); + } + + @Override + public void writeChunk(String streamId, String withData, Callback callback) { + delegate.writeChunk(streamId, withData, callback); + } + + @Override + public void closeStream(String streamId, Callback callback) { + delegate.closeStream(streamId, callback); + } + + @Override + public void unlink(String path, Callback callback) { + delegate.unlink(path, callback); + } + + @Override + public void removeSession(ReadableArray paths, Callback callback) { + delegate.removeSession(paths, callback); + } + + @Override + public void ls(String path, Promise promise) { + delegate.ls(path, promise); + } + + @Override + public void stat(String target, Callback callback) { + delegate.stat(target, callback); + } + + @Override + public void lstat(String path, Callback callback) { + delegate.lstat(path, callback); + } + + @Override + public void cp(String src, String dest, Callback callback) { + delegate.cp(src, dest, callback); + } + + @Override + public void mv(String path, String dest, Callback callback) { + delegate.mv(path, dest, callback); + } + + @Override + public void mkdir(String path, Promise promise) { + delegate.mkdir(path, promise); + } + + @Override + public void readFile(String path, String encoding, boolean transformFile, Promise promise) { + delegate.readFile(path, encoding, transformFile, promise); + } + + @Override + public void hash(String path, String algorithm, Promise promise) { + delegate.hash(path, algorithm, promise); + } + + @Override + public void readStream(String path, String encoding, double bufferSize, double tick, String streamId) { + delegate.readStream(path, encoding, (int) bufferSize, (int) tick, streamId); + } + + @Override + public void getEnvironmentDirs(Callback callback) { + // Not implemented as ReactNativeBlobUtil.getEnvironmentDirs only supports IOS + } + + @Override + public void cancelRequest(String taskId, Callback callback) { + delegate.cancelRequest(taskId, callback); + } + + @Override + public void enableProgressReport(String taskId, double interval, double count) { + delegate.enableProgressReport(taskId, (int) interval, (int) count); + } + + @Override + public void enableUploadProgressReport(String taskId, double interval, double count) { + delegate.enableUploadProgressReport(taskId, (int) interval, (int) count); + } + + @Override + public void slice(String src, String dest, double start, double end, Promise promise) { + delegate.slice(src, dest, (long) start, (long) end, promise); + } + + @Override + public void presentOptionsMenu(String uri, String scheme, Promise promise) { + // Not implemented as ReactNativeBlobUtil.presentOptionsMenu only supports IOS + // This will be rejected at the iOS layer + } + + @Override + public void presentOpenInMenu(String uri, String scheme, Promise promise) { + // Not implemented as ReactNativeBlobUtil.presentOpenInMenu only supports IOS + // This will be rejected at the iOS layer + } + + @Override + public void presentPreview(String uri, String scheme, Promise promise) { + // Not implemented as ReactNativeBlobUtil.presentPreview only supports IOS + // This will be rejected at the iOS layer + } + + @Override + public void excludeFromBackupKey(String url, Promise promise) { + // Not implemented as ReactNativeBlobUtil.excludeFromBackupKey only supports IOS + } + + @Override + public void df(Callback callback) { + delegate.df(callback); + } + + @Override + public void emitExpiredEvent(Callback callback) { + // Not implemented as ReactNativeBlobUtil.emitExpiredEvent only supports IOS + } + + @Override + public void actionViewIntent(String path, String mime, String chooserTitle, Promise promise) { + delegate.actionViewIntent(path, mime, chooserTitle, promise); + } + + @Override + public void addCompleteDownload(ReadableMap config, Promise promise) { + delegate.addCompleteDownload(config, promise); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + @Override + public void copyToInternal(String contentUri, String destpath, Promise promise) { + delegate.copyToInternal(contentUri, destpath, promise); + } + + @Override + public void copyToMediaStore(ReadableMap filedata, String mt, String path, Promise promise) { + delegate.copyToMediaStore(filedata, mt, path, promise); + } + + @Override + public void createMediaFile(ReadableMap filedata, String mt, Promise promise) { + delegate.createMediaFile(filedata, mt, promise); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + @Override + public void getBlob(String contentUri, String encoding, Promise promise) { + delegate.getBlob(contentUri, encoding, promise); + } + + @Override + public void getContentIntent(String mime, Promise promise) { + delegate.getContentIntent(mime, promise); + } + + @Override + public void getSDCardDir(Promise promise) { + delegate.getSDCardDir(promise); + } + + @Override + public void getSDCardApplicationDir(Promise promise) { + delegate.getSDCardApplicationDir(promise); + } + + @Override + public void scanFile(final ReadableArray pairs, final Callback callback) { + delegate.scanFile(pairs, callback); + } + + @Override + public void writeToMediaFile(String fileUri, String path, boolean transformFile, Promise promise) { + delegate.writeToMediaFile(fileUri, path, transformFile, promise); + } +} diff --git a/android/src/oldarch/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java b/android/src/oldarch/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java new file mode 100644 index 000000000..d1ad4289e --- /dev/null +++ b/android/src/oldarch/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java @@ -0,0 +1,253 @@ +package com.ReactNativeBlobUtil; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + +public class ReactNativeBlobUtil extends ReactContextBaseJavaModule { + + private final ReactNativeBlobUtilImpl delegate; + + public ReactNativeBlobUtil(ReactApplicationContext reactContext) { + super(reactContext); + delegate = new ReactNativeBlobUtilImpl(reactContext); + } + + // Required for rn built in EventEmitter Calls. + @ReactMethod + public void addListener(String eventName) { + + } + + @ReactMethod + public void removeListeners(Integer count) { + + } + + @NonNull + @Override + public String getName() { + return ReactNativeBlobUtilImpl.NAME; + } + + @Override + public Map getConstants() { + Map res = new HashMap<>(); + res.putAll(ReactNativeBlobUtilFS.getSystemfolders(this.getReactApplicationContext())); + res.putAll(ReactNativeBlobUtilFS.getLegacySystemfolders(this.getReactApplicationContext())); + + return res; + } + + @ReactMethod + public void createFile(final String path, final String content, final String encode, final Promise promise) { + delegate.createFile(path, content, encode, promise); + } + + @ReactMethod + public void createFileASCII(final String path, final ReadableArray dataArray, final Promise promise) { + delegate.createFileASCII(path, dataArray, promise); + } + + @ReactMethod + public void actionViewIntent(String path, String mime, @Nullable String chooserTitle, final Promise promise) { + delegate.actionViewIntent(path, mime, chooserTitle, promise); + } + + @ReactMethod + public void writeArrayChunk(final String streamId, final ReadableArray dataArray, final Callback callback) { + delegate.writeArrayChunk(streamId, dataArray, callback); + } + + @ReactMethod + public void unlink(String path, Callback callback) { + delegate.unlink(path, callback); + } + + @ReactMethod + public void mkdir(String path, Promise promise) { + delegate.mkdir(path, promise); + } + + @ReactMethod + public void exists(String path, Callback callback) { + delegate.exists(path, callback); + } + + @ReactMethod + public void cp(final String path, final String dest, final Callback callback) { + delegate.cp(path, dest, callback); + } + + @ReactMethod + public void mv(String path, String dest, Callback callback) { + delegate.mv(path, dest, callback); + } + + @ReactMethod + public void ls(String path, Promise promise) { + delegate.ls(path, promise); + } + + @ReactMethod + public void writeStream(String path, String encode, boolean append, Callback callback) { + delegate.writeStream(path, encode, append, callback); + } + + @ReactMethod + public void writeChunk(String streamId, String data, Callback callback) { + delegate.writeChunk(streamId, data, callback); + } + + @ReactMethod + public void closeStream(String streamId, Callback callback) { + delegate.closeStream(streamId, callback); + } + + @ReactMethod + public void removeSession(ReadableArray paths, Callback callback) { + delegate.removeSession(paths, callback); + } + + @ReactMethod + public void readFile(final String path, final String encoding, final boolean transformFile, final Promise promise) { + delegate.readFile(path, encoding, transformFile, promise); + } + + @ReactMethod + public void writeFileArray(final String path, final ReadableArray data, final boolean append, final Promise promise) { + delegate.writeFileArray(path, data, append, promise); + } + + @ReactMethod + public void writeFile(final String path, final String encoding, final String data, final boolean transformFile, final boolean append, final Promise promise) { + delegate.writeFile(path, encoding, data, transformFile, append, promise); + } + + @ReactMethod + public void lstat(String path, Callback callback) { + delegate.lstat(path, callback); + } + + @ReactMethod + public void stat(String path, Callback callback) { + delegate.stat(path, callback); + } + + @ReactMethod + public void scanFile(final ReadableArray pairs, final Callback callback) { + delegate.scanFile(pairs, callback); + } + + @ReactMethod + public void hash(final String path, final String algorithm, final Promise promise) { + delegate.hash(path, algorithm, promise); + } + + /** + * @param path Stream file path + * @param encoding Stream encoding, should be one of `base64`, `ascii`, and `utf8` + * @param bufferSize Stream buffer size, default to 4096 or 4095(base64). + */ + @ReactMethod + public void readStream(final String path, final String encoding, final int bufferSize, final int tick, final String streamId) { + delegate.readStream(path, encoding, bufferSize, tick, streamId); + } + + @ReactMethod + public void cancelRequest(String taskId, Callback callback) { + delegate.cancelRequest(taskId, callback); + } + + @ReactMethod + public void slice(String src, String dest, double start, double end, Promise promise) { + delegate.slice(src, dest, (long) start, (long) end, promise); + } + + @ReactMethod + public void enableProgressReport(String taskId, int interval, int count) { + delegate.enableProgressReport(taskId, interval, count); + } + + @ReactMethod + public void df(final Callback callback) { + delegate.df(callback); + } + + + @ReactMethod + public void enableUploadProgressReport(String taskId, int interval, int count) { + delegate.enableUploadProgressReport(taskId, interval, count); + } + + @ReactMethod + public void fetchBlob(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, final Callback callback) { + delegate.fetchBlob(options, taskId, method, url, headers, body, callback); + } + + @ReactMethod + public void fetchBlobForm(ReadableMap options, String taskId, String method, String url, ReadableMap headers, ReadableArray body, final Callback callback) { + delegate.fetchBlobForm(options, taskId, method, url, headers, body, callback); + } + + @ReactMethod + public void getContentIntent(String mime, Promise promise) { + delegate.getContentIntent(mime, promise); + } + + @ReactMethod + public void addCompleteDownload(ReadableMap config, Promise promise) { + delegate.addCompleteDownload(config, promise); + } + + @ReactMethod + public void getSDCardDir(Promise promise) { + delegate.getSDCardDir(promise); + } + + @ReactMethod + public void getSDCardApplicationDir(Promise promise) { + delegate.getSDCardApplicationDir(promise); + } + + @ReactMethod + public void createMediaFile(ReadableMap filedata, String mt, Promise promise) { + delegate.createMediaFile(filedata, mt, promise); + } + + @ReactMethod + public void writeToMediaFile(String fileUri, String path, boolean transformFile, Promise promise) { + delegate.writeToMediaFile(fileUri, path, transformFile, promise); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + @ReactMethod + public void copyToInternal(String contentUri, String destpath, Promise promise) { + delegate.copyToInternal(contentUri, destpath, promise); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + @ReactMethod + public void getBlob(String contentUri, String encoding, Promise promise) { + delegate.getBlob(contentUri, encoding, promise); + } + + @ReactMethod + public void copyToMediaStore(ReadableMap filedata, String mt, String path, Promise promise) { + delegate.copyToMediaStore(filedata, mt, path, promise); + } +} diff --git a/class/RNFetchBlobFile.js b/class/RNFetchBlobFile.js deleted file mode 100644 index a74927bdc..000000000 --- a/class/RNFetchBlobFile.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2016 wkh237@github. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. - - -import { - NativeModules, - DeviceEventEmitter, - NativeAppEventEmitter, -} from 'react-native' - -const RNFetchBlob = NativeModules.RNFetchBlob -const emitter = DeviceEventEmitter - -export default class RNFetchBlobFile { - -} diff --git a/class/RNFetchBlobReadStream.js b/class/RNFetchBlobReadStream.js deleted file mode 100644 index 84585d99a..000000000 --- a/class/RNFetchBlobReadStream.js +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2016 wkh237@github. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. - -import { - NativeModules, - DeviceEventEmitter, - NativeAppEventEmitter, -} from 'react-native' -import UUID from '../utils/uuid' - -const RNFetchBlob = NativeModules.RNFetchBlob -const emitter = DeviceEventEmitter - -export default class RNFetchBlobReadStream { - - path : string; - encoding : 'utf8' | 'ascii' | 'base64'; - bufferSize : ?number; - closed : boolean; - tick : number = 10; - - constructor(path:string, encoding:string, bufferSize?:?number, tick:number) { - if(!path) - throw Error('RNFetchBlob could not open file stream with empty `path`') - this.encoding = encoding || 'utf8' - this.bufferSize = bufferSize - this.path = path - this.closed = false - this.tick = tick - this._onData = () => {} - this._onEnd = () => {} - this._onError = () => {} - this.streamId = 'RNFBRS'+ UUID() - - // register for file stream event - let subscription = emitter.addListener(this.streamId, (e) => { - let {event, code, detail} = e - if(this._onData && event === 'data') { - this._onData(detail) - return - } - else if (this._onEnd && event === 'end') { - this._onEnd(detail) - } - else { - const err = new Error(detail) - err.code = code || 'EUNSPECIFIED' - if(this._onError) - this._onError(err) - else - throw err - } - // when stream closed or error, remove event handler - if (event === 'error' || event === 'end') { - subscription.remove() - this.closed = true - } - }) - - } - - open() { - if(!this.closed) - RNFetchBlob.readStream(this.path, this.encoding, this.bufferSize || 10240 , this.tick || -1, this.streamId) - else - throw new Error('Stream closed') - } - - onData(fn:() => void) { - this._onData = fn - } - - onError(fn) { - this._onError = fn - } - - onEnd (fn) { - this._onEnd = fn - } - -} diff --git a/class/RNFetchBlobSession.js b/class/RNFetchBlobSession.js deleted file mode 100644 index f40875cb2..000000000 --- a/class/RNFetchBlobSession.js +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2016 wkh237@github. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. - -import { - NativeModules, - DeviceEventEmitter, - NativeAppEventEmitter, -} from 'react-native' - -const RNFetchBlob = NativeModules.RNFetchBlob - -let sessions = {} - -export default class RNFetchBlobSession { - - name : string; - - static getSession(name:string):any { - return sessions[name] - } - - static setSession(name:string, val:any) { - sessions[name] = val - } - - static removeSession(name:string) { - delete sessions[name] - } - - constructor(name:string, list:Array) { - this.name = name - if(!sessions[name]) { - if(Array.isArray(list)) - sessions[name] = list - else - sessions[name] = [] - } - } - - add(path:string):RNFetchBlobSession { - sessions[this.name].push(path) - return this - } - - remove(path:string):RNFetchBlobSession { - let list = sessions[this.name] - for(let i of list) { - if(list[i] === path) { - sessions[this.name].splice(i, 1) - break; - } - } - return this - } - - list():Array { - return sessions[this.name] - } - - dispose():Promise { - return new Promise((resolve, reject) => { - RNFetchBlob.removeSession(sessions[this.name], (err) => { - if(err) - reject(new Error(err)) - else { - delete sessions[this.name] - resolve() - } - }) - }) - } - -} diff --git a/class/RNFetchBlobWriteStream.js b/class/RNFetchBlobWriteStream.js deleted file mode 100644 index d88c48e22..000000000 --- a/class/RNFetchBlobWriteStream.js +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2016 wkh237@github. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. - -import { - NativeModules, - DeviceEventEmitter, - NativeAppEventEmitter, -} from 'react-native' - -const RNFetchBlob = NativeModules.RNFetchBlob - -export default class RNFetchBlobWriteStream { - - id : string; - encoding : string; - append : boolean; - - constructor(streamId:string, encoding:string, append:boolean) { - this.id = streamId - this.encoding = encoding - this.append = append - } - - write(data:string): Promise { - return new Promise((resolve, reject) => { - try { - let method = this.encoding === 'ascii' ? 'writeArrayChunk' : 'writeChunk' - if(this.encoding.toLocaleLowerCase() === 'ascii' && !Array.isArray(data)) { - reject(new Error('ascii input data must be an Array')) - return - } - RNFetchBlob[method](this.id, data, (error) => { - if(error) - reject(new Error(error)) - else - resolve(this) - }) - } catch(err) { - reject(new Error(err)) - } - }) - } - - close() { - return new Promise((resolve, reject) => { - try { - RNFetchBlob.closeStream(this.id, () => { - resolve() - }) - } catch (err) { - reject(new Error(err)) - } - }) - } - -} diff --git a/class/ReactNativeBlobUtilBlobResponse.js b/class/ReactNativeBlobUtilBlobResponse.js new file mode 100644 index 000000000..afa6fed5f --- /dev/null +++ b/class/ReactNativeBlobUtilBlobResponse.js @@ -0,0 +1,177 @@ +import {ReactNativeBlobUtilResponseInfo, ReactNativeBlobUtilStream} from "../types"; +import fs from "../fs"; +import Blob from "../polyfill/Blob"; +import ReactNativeBlobUtilSession from "./ReactNativeBlobUtilSession"; +import URIUtil from "../utils/uri"; +import base64 from "base-64"; +/** + * ReactNativeBlobUtil response object class. + */ +export class FetchBlobResponse { + + taskId: string; + path: () => string | null; + type: 'base64' | 'path' | 'utf8'; + data: any; + blob: (contentType: string, sliceSize: number) => Promise; + text: () => string | Promise; + json: () => any; + base64: () => any; + flush: () => void; + respInfo: ReactNativeBlobUtilResponseInfo; + session: (name: string) => ReactNativeBlobUtilSession | null; + readFile: (encode: 'base64' | 'utf8' | 'ascii') => ?Promise; + readStream: ( + encode: 'utf8' | 'ascii' | 'base64', + ) => ReactNativeBlobUtilStream | null; + + constructor(taskId: string, info: ReactNativeBlobUtilResponseInfo, data: any) { + this.data = data; + this.taskId = taskId; + this.type = info.rnfbEncode; + this.respInfo = info; + + this.info = (): ReactNativeBlobUtilResponseInfo => { + return this.respInfo; + }; + + this.array = (): Promise => { + let cType = info.headers['Content-Type'] || info.headers['content-type']; + return new Promise((resolve, reject) => { + switch (this.type) { + case 'base64': + // TODO : base64 to array buffer + break; + case 'path': + fs.readFile(this.data, 'ascii').then(resolve); + break; + default: + // TODO : text to array buffer + break; + } + }); + }; + + /** + * Convert result to javascript ReactNativeBlobUtil object. + * @return {Promise} Return a promise resolves Blob object. + */ + this.blob = (): Promise => { + let cType = info.headers['Content-Type'] || info.headers['content-type']; + return new Promise((resolve, reject) => { + switch (this.type) { + case 'base64': + Blob.build(this.data, {type: cType + ';BASE64'}).then(resolve); + break; + case 'path': + Blob.build(URIUtil.wrap(this.data), {type: cType}).then(resolve); + break; + default: + Blob.build(this.data, {type: 'text/plain'}).then(resolve); + break; + } + }); + }; + /** + * Convert result to text. + * @return {string} Decoded base64 string. + */ + this.text = (): string | Promise => { + switch (this.type) { + case 'base64': + return base64.decode(this.data); + case 'path': + return fs.readFile(this.data, 'base64').then((b64) => Promise.resolve(base64.decode(b64))); + default: + return this.data; + } + }; + /** + * Convert result to JSON object. + * @return {object} Parsed javascript object. + */ + this.json = (): any => { + switch (this.type) { + case 'base64': + return JSON.parse(base64.decode(this.data)); + case 'path': + return fs.readFile(this.data, 'utf8') + .then((text) => Promise.resolve(JSON.parse(text))); + default: + return JSON.parse(this.data); + } + }; + /** + * Return BASE64 string directly. + * @return {string} BASE64 string of response body. + */ + this.base64 = (): string | Promise => { + switch (this.type) { + case 'base64': + return this.data; + case 'path': + return fs.readFile(this.data, 'base64'); + default: + return base64.encode(this.data); + } + }; + /** + * Remove cahced file + * @return {Promise} + */ + this.flush = () => { + let path = this.path(); + if (!path || this.type !== 'path') + return; + return fs.unlink(path); + }; + /** + * get path of response temp file + * @return {string} File path of temp file. + */ + this.path = () => { + if (this.type === 'path') + return this.data; + return null; + }; + + this.session = (name: string): ReactNativeBlobUtilSession | null => { + if (this.type === 'path') + return fs.session(name).add(this.data); + else { + console.warn('only file paths can be add into session.'); + return null; + } + }; + /** + * Start read stream from cached file + * @param {String} encoding Encode type, should be one of `base64`, `ascii`, `utf8`. + * @return {void} + */ + this.readStream = (encoding: 'base64' | 'utf8' | 'ascii'): ReactNativeBlobUtilStream | null => { + if (this.type === 'path') { + return fs.readStream(this.data, encoding); + } + else { + console.warn('ReactNativeBlobUtil', 'this response data does not contains any available stream'); + return null; + } + }; + /** + * Read file content with given encoding, if the response does not contains + * a file path, show warning message + * @param {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`. + * @return {String} + */ + this.readFile = (encoding: 'base64' | 'utf8' | 'ascii') => { + if (this.type === 'path') { + return fs.readFile(this.data, encoding); + } + else { + console.warn('ReactNativeBlobUtil', 'this response does not contains a readable file'); + return null; + } + }; + } + +} \ No newline at end of file diff --git a/class/ReactNativeBlobUtilCanceledFetchError.js b/class/ReactNativeBlobUtilCanceledFetchError.js new file mode 100644 index 000000000..9cbadd864 --- /dev/null +++ b/class/ReactNativeBlobUtilCanceledFetchError.js @@ -0,0 +1,10 @@ +const CANCELED_FETCH_ERROR_NAME = 'ReactNativeBlobUtilCanceledFetch' + +class CanceledFetchError extends Error { + constructor(message) { + super(message); + this.name = CANCELED_FETCH_ERROR_NAME + } +} + +export default CanceledFetchError \ No newline at end of file diff --git a/class/ReactNativeBlobUtilFile.js b/class/ReactNativeBlobUtilFile.js new file mode 100644 index 000000000..db29f4d4d --- /dev/null +++ b/class/ReactNativeBlobUtilFile.js @@ -0,0 +1,5 @@ +// Copyright 2016 wkh237@github. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +export default class ReactNativeBlobUtilFile {} diff --git a/class/ReactNativeBlobUtilReadStream.js b/class/ReactNativeBlobUtilReadStream.js new file mode 100644 index 000000000..490efb178 --- /dev/null +++ b/class/ReactNativeBlobUtilReadStream.js @@ -0,0 +1,84 @@ +// Copyright 2016 wkh237@github. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +import {NativeEventEmitter} from 'react-native'; +import UUID from '../utils/uuid'; + +import ReactNativeBlobUtil from '../codegenSpecs/NativeBlobUtils'; + +const emitter = new NativeEventEmitter(ReactNativeBlobUtil); + +export default class ReactNativeBlobUtilReadStream { + + path: string; + encoding: 'utf8' | 'ascii' | 'base64'; + bufferSize: ?number; + closed: boolean; + tick: number = 10; + + constructor(path: string, encoding: string, bufferSize?: ?number, tick: number) { + if (!path) + throw Error('ReactNativeBlobUtil could not open file stream with empty `path`'); + this.encoding = encoding || 'utf8'; + this.bufferSize = bufferSize; + this.path = path; + this.closed = false; + this.tick = tick; + this._onData = () => { + }; + this._onEnd = () => { + }; + this._onError = () => { + }; + this.streamId = 'RNFBRS' + UUID(); + + // register for file stream event + let subscription = emitter.addListener('ReactNativeBlobUtilFilesystem', (e) => { + if (typeof e === 'string') e = JSON.parse(e); + if (e.streamId !== this.streamId) return; // wrong stream + let {event, code, detail} = e; + if (this._onData && event === 'data') { + this._onData(detail); + return; + } + else if (this._onEnd && event === 'end') { + this._onEnd(detail); + } + else { + const err = new Error(detail); + err.code = code || 'EUNSPECIFIED'; + if (this._onError) + this._onError(err); + else + throw err; + } + // when stream closed or error, remove event handler + if (event === 'error' || event === 'end') { + subscription.remove(); + this.closed = true; + } + }); + + } + + open() { + if (!this.closed) + ReactNativeBlobUtil.readStream(this.path, this.encoding, this.bufferSize || 10240, this.tick || -1, this.streamId); + else + throw new Error('Stream closed'); + } + + onData(fn: () => void) { + this._onData = fn; + } + + onError(fn) { + this._onError = fn; + } + + onEnd(fn) { + this._onEnd = fn; + } + +} diff --git a/class/ReactNativeBlobUtilSession.js b/class/ReactNativeBlobUtilSession.js new file mode 100644 index 000000000..053282f48 --- /dev/null +++ b/class/ReactNativeBlobUtilSession.js @@ -0,0 +1,68 @@ +// Copyright 2016 wkh237@github. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +import ReactNativeBlobUtil from '../codegenSpecs/NativeBlobUtils'; + +let sessions = {}; + +export default class ReactNativeBlobUtilSession { + + name : string; + + static getSession(name:string):any { + return sessions[name]; + } + + static setSession(name:string, val:any) { + sessions[name] = val; + } + + static removeSession(name:string) { + delete sessions[name]; + } + + constructor(name:string, list:Array) { + this.name = name; + if (!sessions[name]) { + if (Array.isArray(list)) + sessions[name] = list; + else + sessions[name] = []; + } + } + + add(path:string):ReactNativeBlobUtilSession { + sessions[this.name].push(path); + return this; + } + + remove(path:string):ReactNativeBlobUtilSession { + let list = sessions[this.name]; + for (let i = 0; i < list.length; i++) { + if (list[i] === path) { + sessions[this.name].splice(i, 1); + break; + } + } + return this; + } + + list():Array { + return sessions[this.name]; + } + + dispose():Promise { + return new Promise((resolve, reject) => { + ReactNativeBlobUtil.removeSession(sessions[this.name], (err) => { + if (err) + reject(new Error(err)); + else { + delete sessions[this.name]; + resolve(); + } + }); + }); + } + +} diff --git a/class/ReactNativeBlobUtilWriteStream.js b/class/ReactNativeBlobUtilWriteStream.js new file mode 100644 index 000000000..32b7bc657 --- /dev/null +++ b/class/ReactNativeBlobUtilWriteStream.js @@ -0,0 +1,50 @@ +// Copyright 2016 wkh237@github. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +import ReactNativeBlobUtil from '../codegenSpecs/NativeBlobUtils'; +export default class ReactNativeBlobUtilWriteStream { + + id : string; + encoding : string; + append : boolean; + + constructor(streamId:string, encoding:string, append:boolean) { + this.id = streamId; + this.encoding = encoding; + this.append = append; + } + + write(data:string): Promise { + return new Promise((resolve, reject) => { + try { + let method = this.encoding === 'ascii' ? 'writeArrayChunk' : 'writeChunk'; + if (this.encoding.toLocaleLowerCase() === 'ascii' && !Array.isArray(data)) { + reject(new Error('ascii input data must be an Array')); + return; + } + ReactNativeBlobUtil[method](this.id, data, (error) => { + if (error) + reject(new Error(error)); + else + resolve(this); + }); + } catch (err) { + reject(new Error(err)); + } + }); + } + + close() { + return new Promise((resolve, reject) => { + try { + ReactNativeBlobUtil.closeStream(this.id, () => { + resolve(); + }); + } catch (err) { + reject(new Error(err)); + } + }); + } + +} diff --git a/codegenSpecs/NativeBlobUtils.js b/codegenSpecs/NativeBlobUtils.js new file mode 100644 index 000000000..a8da97f3d --- /dev/null +++ b/codegenSpecs/NativeBlobUtils.js @@ -0,0 +1,86 @@ +// @flow +import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + +getConstants: () => {| + CacheDir: string, + DocumentDir: string, + DownloadDir: string, + LibraryDir: string, + MainBundleDir: string, + MovieDir: string, + MusicDir: string, + PictureDir: string, + ApplicationSupportDir: string, + // Android Only Constants + RingtoneDir: string, + SDCardDir: string, + SDCardApplicationDir: string, + DCIMDir: string, + // Android Only Legacy Constants + LegacyDCIMDir: string, + LegacyPictureDir: string, + LegacyMusicDir: string, + LegacyDownloadDir: string, + LegacyMovieDir: string, + LegacyRingtoneDir: string, + LegacySDCardDir: string, + |}; + + +fetchBlobForm: (options: Object, taskId: string, method: string, url: string, headers: Object, form: Array, callback: (value: Array) => void) => void; + +fetchBlob: (options: Object, taskId: string, method: string, url: string, headers: Object, body: string, callback: (value: Array) => void) => void; + +createFile: (path: string, data: string, encoding: string) => Promise; + +createFileASCII: (path: string, data: Array) => Promise; + +pathForAppGroup: (groupName: string) => Promise; + +syncPathAppGroup: (groupName: string) => string; + +exists: (path: string, callback: (value: Array) => void) => void; + +writeFile: (path: string, encoding: string, data: string, transformFile: boolean, append: boolean) => Promise; + +writeFileArray: (path: string, data: Array, append: boolean) => Promise; + +writeStream: (path: string, withEncoding: string, appendData: boolean, callback: (value: Array) => void) => void; + +writeArrayChunk: (streamId: string, withArray: Array, callback: (value: Array) => void) => void; + +writeChunk: (streamId: string, withData: string, callback: (value: Array) => void) => void; + +closeStream: (streamId: string, callback: (value: Array) => void) => void; + +unlink: (path: string, callback: (value: Array) => void) => void; + +removeSession: (paths: Array, callback: (value: Array) => void) => void; + +ls: (path: string) => Promise>; + +stat: (target: string, callback: (value: Array) => void) => void; + +lstat: (path: string, callback: (value: Array) => void) => void; + +cp: (src: string, dest: string, callback: (value: Array) => void) => void; + +mv: (path: string, dest: string, callback: (value: Array) => void) => void; + +mkdir: (path: string) => Promise; + +readFile: (path: string, encoding: string, transformFile: boolean) => Promise>; + +hash: (path: string, algorithm: string) => Promise; + +readStream: (path: string, encoding: string, bufferSize: number, tick: number, streamId: string) => void; + +getEnvironmentDirs: (callback: (value: Array) => void) => void; + +cancelRequest: (taskId: string, callback: (value: Array) => void) => void; + +enableProgressReport: (taskId: string, interval: number, count: number) => void; + +enableUploadProgressReport: (taskId: string, interval: number, count: number) => void; + +slice: (src: string, dest: string, start: number, end: number) => Promise; + +presentOptionsMenu: (uri: string, scheme: string) => Promise>; + +presentOpenInMenu: (uri: string, scheme: string) => Promise>; + +presentPreview: (uri: string, scheme: string) => Promise>; + +excludeFromBackupKey: (url: string) => Promise>; + +df: (callback: (value: Array) => void) => void; + +emitExpiredEvent: (callback: (value: string) => void) => void; // The callback is not really used here + // Android Only APIs + +actionViewIntent: (path: string, mime: string, chooserTitle: string) => Promise; + +addCompleteDownload: (config: Object) => Promise; + +copyToInternal: (contentUri: string, destpath: string) => Promise; + +copyToMediaStore: (filedata: Object, mt: string, path: string) => Promise; + +createMediaFile: (filedata: Object, mt: string) => Promise; + +getBlob: (contentUri: string, encoding: string) => Promise>; + +getContentIntent: (mime: string) => Promise; + +getSDCardDir: () => Promise; + +getSDCardApplicationDir: () => Promise; + +scanFile: (pairs: Array, callback: (value: Array) => void) => void; + +writeToMediaFile: (fileUri: string, path: string, transformFile: boolean) => Promise; + + // RCTEventEmitter + +addListener: (eventName: string) => void; + +removeListeners: (count: number) => void; + } + + export default (TurboModuleRegistry.get('ReactNativeBlobUtil'): ?Spec); + + diff --git a/examples/.npmignore b/examples/.npmignore new file mode 100644 index 000000000..1a5d75c5a --- /dev/null +++ b/examples/.npmignore @@ -0,0 +1,4 @@ +# Make sure we don't publish examples +ReactNativeBlobUtilWin/ +NewArch/ +OldArch/ diff --git a/examples/ReactNativeBlobUtil/.buckconfig b/examples/ReactNativeBlobUtil/.buckconfig new file mode 100644 index 000000000..934256cb2 --- /dev/null +++ b/examples/ReactNativeBlobUtil/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/examples/ReactNativeBlobUtil/.eslintrc.js b/examples/ReactNativeBlobUtil/.eslintrc.js new file mode 100644 index 000000000..7b46a68c0 --- /dev/null +++ b/examples/ReactNativeBlobUtil/.eslintrc.js @@ -0,0 +1,56 @@ +module.exports = { + root: true, + extends: '@react-native-community', + rules: { + 'prettier/prettier': 0, + "eqeqeq": 2, + "comma-dangle": 0, + curly: 0, + "no-console": 1, + "no-debugger": 1, + "no-extra-semi": 2, + "no-extra-parens": 1, + "no-extra-boolean-cast": 1, + "no-cond-assign": 2, + "no-irregular-whitespace": 2, + "no-undef": 0, + "no-unused-vars": 0, + "semi": 2, + "semi-spacing": 2, + "valid-jsdoc": [ + 1, + { + "requireReturn": false, + "requireParamDescription": false, + "requireReturnDescription": false + } + ], + "radix": 0, + + "react/display-name": 2, + "react/forbid-prop-types": 1, + "react/jsx-boolean-value": 1, + "react/jsx-closing-bracket-location": 1, + "react/jsx-curly-spacing": 1, + "react/jsx-indent-props": 0, + "react/jsx-max-props-per-line": 0, + "react/jsx-no-duplicate-props": 1, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 1, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/jsx-wrap-multilines": 1, + "react/no-danger": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-direct-mutation-state": 1, + "react/no-multi-comp": 1, + "react/no-set-state": 0, + "react/no-unknown-property": 1, + "react/prop-types": 0, + "react/react-in-jsx-scope": 0, + "react/self-closing-comp": 1, + "react/sort-comp": 1, + }, +}; diff --git a/examples/ReactNativeBlobUtil/.flowconfig b/examples/ReactNativeBlobUtil/.flowconfig new file mode 100644 index 000000000..b274ad1d6 --- /dev/null +++ b/examples/ReactNativeBlobUtil/.flowconfig @@ -0,0 +1,73 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore polyfills +node_modules/react-native/Libraries/polyfills/.* + +; These should not be required directly +; require from fbjs/lib instead: require('fbjs/lib/warning') +node_modules/warning/.* + +; Flow doesn't support platforms +.*/Libraries/Utilities/LoadingView.js + +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + +[include] + +[libs] +node_modules/react-native/interface.js +node_modules/react-native/flow/ + +[options] +emoji=true + +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable + +module.file_ext=.js +module.file_ext=.json +module.file_ext=.ios.js + +munge_underscores=true + +module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' +module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FlowFixMeProps +suppress_type=$FlowFixMeState + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError + +[lints] +sketchy-null-number=warn +sketchy-null-mixed=warn +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +unnecessary-invariant=warn +signature-verification-failure=warn +deprecated-utility=error + +[strict] +deprecated-type +nonstrict-import +sketchy-null +unclear-type +unsafe-getters-setters +untyped-import +untyped-type-import + +[version] +^0.122.0 diff --git a/examples/ReactNativeBlobUtil/.gitattributes b/examples/ReactNativeBlobUtil/.gitattributes new file mode 100644 index 000000000..d42ff1835 --- /dev/null +++ b/examples/ReactNativeBlobUtil/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/examples/ReactNativeBlobUtil/.gitignore b/examples/ReactNativeBlobUtil/.gitignore new file mode 100644 index 000000000..ad572e632 --- /dev/null +++ b/examples/ReactNativeBlobUtil/.gitignore @@ -0,0 +1,59 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ diff --git a/examples/ReactNativeBlobUtil/.prettierrc.js b/examples/ReactNativeBlobUtil/.prettierrc.js new file mode 100644 index 000000000..5c4de1a4f --- /dev/null +++ b/examples/ReactNativeBlobUtil/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: false, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/examples/ReactNativeBlobUtil/.watchmanconfig b/examples/ReactNativeBlobUtil/.watchmanconfig new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/examples/ReactNativeBlobUtil/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/examples/ReactNativeBlobUtil/App.js b/examples/ReactNativeBlobUtil/App.js new file mode 100644 index 000000000..eade510c8 --- /dev/null +++ b/examples/ReactNativeBlobUtil/App.js @@ -0,0 +1,732 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + * @format + * @flow strict-local + */ + +import React, {useState} from 'react'; +import {Alert, Button, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, View} from 'react-native'; +import {Picker} from '@react-native-picker/picker'; + +import {Colors} from 'react-native/Libraries/NewAppScreen'; + +import ReactNativeBlobUtil from 'react-native-blob-util'; + +const App: () => React$Node = () => { + // Variables ****************************************************************** + const [existsParam, setExistsParam] = useState(''); + const [lsParam, setLSParam] = useState(''); + + const [cpSourceParam, setCPSourceParam] = useState(''); + const [cpDestParam, setCPDestParam] = useState(''); + + const [unlinkParam, setUnlinkParam] = useState(''); + + const [statParam, setStatParam] = useState(''); + + const [mkdirParam, setMkdirParam] = useState(''); + const [mkdirURIParam, setMkdirURIParam] = useState(''); + + const [readParam, setReadParam] = useState(''); + + const [hashPathParam, setHashPathParam] = useState(''); + const [hashAlgValue, setHashAlgValue] = useState('md5'); + + const [writeParam, setWriteParam] = useState(''); + const [writeURIParam, setWriteURIParam] = useState(''); + const [writeEncodeParam, setWriteEncodeParam] = useState('utf8'); + + const [writeStreamParam, setWriteStreamParam] = useState(''); + const [writeEncodeStreamParam, setWriteStreamEncodeParam] = useState('utf8'); + + const [readStreamParam, setReadStreamParam] = useState(''); + const [readEncodeStreamParam, setReadStreamEncodeParam] = useState('utf8'); + + // Methods ******************************************************************** + // exists() + const existsCall = () => { + ReactNativeBlobUtil.fs + .exists(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + existsParam) + .then((result) => { + Alert.alert('Exists: ' + result); + }) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + const isDirCall = () => { + ReactNativeBlobUtil.fs + .exists(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + existsParam) + .then((result) => { + Alert.alert('isDir: ' + result); + }) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + // df() + const dfCall = () => { + ReactNativeBlobUtil.fs + .df() + .then((result) => { + Alert.alert('Free space: ' + result.free + ' bytes\nTotal space: ' + result.total + ' bytes'); + }) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + // ls() + const lsCall = () => { + ReactNativeBlobUtil.fs.ls(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + lsParam).then((files) => { + Alert.alert('Method finished: check debug console for results'); + console.log(files); + }); + }; + + // cp() + const cpCall = () => { + ReactNativeBlobUtil.fs + .cp(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + cpSourceParam, ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + cpDestParam) + .then(Alert.alert('File successfully copied')) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + // mv() + const mvCall = () => { + ReactNativeBlobUtil.fs + .mv(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + cpSourceParam, ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + cpDestParam) + .then(Alert.alert('File successfully moved')) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + // unlink() + const unlinkCall = () => { + ReactNativeBlobUtil.fs + .unlink(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + unlinkParam) + .then(Alert.alert('file/directory successfully unlinked')) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + // stat(), lstat() + const statCall = () => { + ReactNativeBlobUtil.fs + .stat(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + statParam) + .then((stats) => { + console.log(stats); + Alert.alert('stat() result (others logged in console)', 'filename: ' + stats.filename + '\nlastModified: ' + stats.lastModified + '\npath: ' + stats.path + '\nsize: ' + stats.size + '\ntype: ' + stats.type); + }) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + const lstatCall = () => { + ReactNativeBlobUtil.fs + .lstat(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + statParam) + .then((stats) => { + console.log(stats); + Alert.alert('lstat() result (others logged in console)', 'filename: ' + stats[0].filename + '\nlastModified: ' + stats[0].lastModified + '\npath: ' + stats[0].path + '\nsize: ' + stats[0].size + '\ntype: ' + stats[0].type); + }) + .catch((err) => { + Alert.alert(err.message); + }); + }; + + // mkdir() + const mkdirCall = () => { + if (mkdirParam.length > 0) { + ReactNativeBlobUtil.fs + .mkdir(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirParam) + .then(() => { + Alert.alert('successfully created file:', ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirParam); + }) + .catch((err) => { + Alert.alert(err.message); + }); + } else { + Alert.alert('Cannot make file with no name provided'); + } + }; + + // createFile() + const createFileUTF8Call = () => { + ReactNativeBlobUtil.fs.createFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirParam, 'foo', 'utf8'); + }; + + const createFileASCIICall = () => { + ReactNativeBlobUtil.fs.createFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirParam, [102, 111, 111], 'ascii'); + }; + + const createFileBase64Call = () => { + ReactNativeBlobUtil.fs.createFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirParam, 'Zm9v', 'base64'); + }; + + const createFileURICall = () => { + ReactNativeBlobUtil.fs.createFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirParam, ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + mkdirURIParam, 'uri'); + }; + + // readFile() + const readFileUTF8Call = () => { + ReactNativeBlobUtil.fs.readFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + readParam, 'utf8').then((data) => { + Alert.alert('UTF8 result of ' + readParam, data); + }); + }; + + const readFileASCIICall = () => { + ReactNativeBlobUtil.fs.readFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + readParam, 'ascii').then((data) => { + Alert.alert('UTF8 result of ' + readParam, data); + }); + }; + + const readFileBase64Call = () => { + ReactNativeBlobUtil.fs.readFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + readParam, 'base64').then((data) => { + Alert.alert('UTF8 result of ' + readParam, data); + }); + }; + + // hash() + const hashCall = () => { + ReactNativeBlobUtil.fs + .hash(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + hashPathParam, hashAlgValue) + .then((hash) => { + Alert.alert(hashAlgValue, hash); + }) + .catch((err) => { + console.log(hashAlgValue + ': ' + err); + }); + }; + + // writeFile() + const writeFileCall = () => { + if (writeParam.length > 0) { + if (writeEncodeParam === 'uri') { + if (writeURIParam.length > 0) { + ReactNativeBlobUtil.fs.writeFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeParam, ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeURIParam, writeEncodeParam); + } else { + Alert.alert('uri path undefined'); + } + } else if (writeEncodeParam === 'ascii') { + ReactNativeBlobUtil.fs.writeFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeParam, [102, 111, 111], writeEncodeParam); + } else { + ReactNativeBlobUtil.fs.writeFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeParam, 'foo', writeEncodeParam); + } + } + }; + + // appendFile() + const appendFileCall = () => { + if (writeParam.length > 0) { + if (writeEncodeParam === 'uri') { + if (writeURIParam.length > 0) { + ReactNativeBlobUtil.fs.appendFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeParam, ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeURIParam, writeEncodeParam); + } else { + Alert.alert('uri path undefined'); + } + } else if (writeEncodeParam === 'ascii') { + ReactNativeBlobUtil.fs.appendFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeParam, [102, 111, 111], writeEncodeParam); + } else { + ReactNativeBlobUtil.fs.appendFile(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeParam, 'foo', writeEncodeParam); + } + } + }; + + const writeStreamCall = () => { + if (writeStreamParam.length > 0) { + if (writeEncodeStreamParam === 'base64') { + ReactNativeBlobUtil.fs.writeStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeStreamParam, writeEncodeStreamParam, false).then((stream) => { + stream.write('Zm9vIChXcml0ZSBCYXNlNjQpMQ=='); + stream.write('Zm9vIChXcml0ZSBCYXNlNjQpMg=='); + return stream.close(); + }); + } else if (writeEncodeStreamParam === 'ascii') { + ReactNativeBlobUtil.fs.writeStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeStreamParam, writeEncodeStreamParam, false).then((stream) => { + stream.write([102, 111, 111, 32, 40, 87, 114, 105, 116]); + stream.write([101, 32, 97, 115, 99, 105, 105, 41]); + return stream.close(); + }); + } else { + ReactNativeBlobUtil.fs.writeStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeStreamParam, writeEncodeStreamParam, false).then((stream) => { + stream.write('foo (Write utf8)1'); + stream.write('foo (Write utf8)2'); + return stream.close(); + }); + } + } + }; + + const appendStreamCall = () => { + if (writeStreamParam.length > 0) { + if (writeEncodeStreamParam === 'base64') { + ReactNativeBlobUtil.fs.writeStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeStreamParam, writeEncodeStreamParam, true).then((stream) => { + stream.write('Zm9vIChBcHBlbmQgQmFzZTY0KTE='); + stream.write('Zm9vIChBcHBlbmQgQmFzZTY0KTI='); + return stream.close(); + }); + } else if (writeEncodeStreamParam === 'ascii') { + ReactNativeBlobUtil.fs.writeStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeStreamParam, writeEncodeStreamParam, true).then((stream) => { + stream.write([102, 111, 111, 32, 40]); + stream.write([65, 112, 112, 101, 110, 100, 32, 65, 83, 67, 73, 73, 41]); + return stream.close(); + }); + } else { + ReactNativeBlobUtil.fs.writeStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + writeStreamParam, writeEncodeStreamParam, true).then((stream) => { + stream.write('foo (Append utf8)1'); + stream.write('foo (Append utf8)2'); + return stream.close(); + }); + } + } + }; + + // readStream + const readStreamCall = () => { + ReactNativeBlobUtil.fs.readStream(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/' + readStreamParam, readEncodeStreamParam, 4000, 200).then((stream) => { + let data = ''; + stream.open(); + stream.onData((chunk) => { + data += chunk; + }); + stream.onEnd(() => { + console.log('data: ' + data); + }); + }); + }; + + // fetchCall + const fetchCall = () => { + ReactNativeBlobUtil.config({ + // add this option that makes response data to be stored as a file, + // this is much more performant. + fileCache: true, + }) + .fetch('GET', 'https://upload.wikimedia.org/wikipedia/commons/c/c4/Change-5.png') + .then((res) => { + // the temp file path + console.log('The file saved to ', res.path()); + }); + }; + + // Android mediastorage store + const androidmediastore = () => { + ReactNativeBlobUtil.config({ + // add this option that makes response data to be stored as a file, + // this is much more performant. + fileCache: true, + }) + .fetch('GET', 'https://upload.wikimedia.org/wikipedia/commons/c/c4/Change-5.png') + .then((res) => { + // the temp file path + ReactNativeBlobUtil.MediaCollection.copyToMediaStore( + { + name: 'test.png', + parentFolder: '', + mimeType: 'image/png', + }, + 'Download', + res.path(), + ).then((dest) => ReactNativeBlobUtil.android.actionViewIntent(dest, 'image/png')); + }); + }; + + // uploadFileFromStorage + const uploadFromStorageCall = () => { + ReactNativeBlobUtil.fetch( + 'POST', + 'https://enb954aqyumba.x.pipedream.net/', + { + Authorization: 'Bearer access-token...', + 'Dropbox-API-Arg': JSON.stringify({ + path: '/img-from-react-native.png', + mode: 'add', + autorename: true, + mute: false, + }), + 'Content-Type': 'application/octet-stream', + // here's the body you're going to send, should be a BASE64 encoded string + // (you can use "base64"(refer to the library 'mathiasbynens/base64') APIs to make one). + // The data will be converted to "byte array"(say, blob) before request sent. + }, + ReactNativeBlobUtil.wrap(ReactNativeBlobUtil.fs.dirs.DocumentDir + '\\ImageToUpload.jpg'), + ) + .then((res) => { + console.log(res.text()); + }) + .catch((err) => { + // error handling .. + }); + }; + + // uploadTextFromStorage + const uploadTextFromCall = () => { + ReactNativeBlobUtil.fetch( + 'POST', + 'https://enb954aqyumba.x.pipedream.net/', + { + Authorization: 'Bearer access-token...', + 'Dropbox-API-Arg': JSON.stringify({ + path: '/img-from-react-native.png', + mode: 'add', + autorename: true, + mute: false, + }), + 'Content-Type': 'application/octet-stream', + // here's the body you're going to send, should be a BASE64 encoded string + // (you can use "base64"(refer to the library 'mathiasbynens/base64') APIs to make one). + // The data will be converted to "byte array"(say, blob) before request sent. + }, + 'Waka Flacka Flame goes very well with Thomas the Tank Engine.', + ) + .then((res) => { + console.log(res.text()); + }) + .catch((err) => { + // error handling .. + }); + }; + + // MultipartFileAndData + const MultipartFileAndData = () => { + ReactNativeBlobUtil.fetch( + 'POST', + 'https://enb954aqyumba.x.pipedream.net/', + { + Authorization: 'Bearer access-token...', + 'Dropbox-API-Arg': JSON.stringify({ + path: '/img-from-react-native.png', + mode: 'add', + autorename: true, + mute: false, + }), + 'Content-Type': 'application/octet-stream', + // here's the body you're going to send, should be a BASE64 encoded string + // (you can use "base64"(refer to the library 'mathiasbynens/base64') APIs to make one). + // The data will be converted to "byte array"(say, blob) before request sent. + }, + 'Waka Flacka Flame goes very well with Thomas the Tank Engine.', + ) + .uploadProgress((received, total) => { + console.log('upload progress', received / total); + }) + .progress((received, total) => { + console.log('download progress', received / total); + }) + .then((res) => { + console.log(res.text()); + }) + .catch((err) => { + // error handling .. + }); + }; + + // + const MakeRequestWithProgress = () => { + ReactNativeBlobUtil.config({ + // add this option that makes response data to be stored as a file, + // this is much more performant. + fileCache: true, + }) + .fetch( + 'POST', + 'https://enb954aqyumba.x.pipedream.net/', + { + Authorization: 'Bearer access-token', + otherHeader: 'foo', + 'Content-Type': 'multipart/form-data', + }, + [ + // element with property `filename` will be transformed into `file` in form data + {name: 'avatar', filename: 'avatar.png', data: 'Kentucky Fried Seth'}, + // custom content type + { + name: 'avatar-png', + filename: 'avatar-png.png', + type: 'image/png', + data: 'whaddup my pickles', + }, + // part file from storage + { + name: 'avatar-foo', + filename: 'avatar-foo.png', + type: 'image/foo', + data: ReactNativeBlobUtil.wrap(ReactNativeBlobUtil.fs.dirs.DocumentDir + '\\ImageToUpload.jpg'), + }, + // elements without property `filename` will be sent as plain text + {name: 'name', data: 'user'}, + { + name: 'info', + data: JSON.stringify({ + mail: 'example@example.com', + tel: '12345678', + }), + }, + ], + ) + .uploadProgress({interval: 250}, (written, total) => { + console.log('uploaded', written / total); + }) + .progress({count: 10, interval: -1}, (received, total) => { + console.log('progress', received / total); + }) + .then((res) => { + console.log(res.text()); + }) + .catch((err) => { + console.log(err.text()); + }); + }; + + // App ************************************************************************ + return ( + <> + + + + {global.HermesInternal == null ? null : ( + + Engine: Hermes + + )} + {'React Native Fetch Blob Windows Demo App'} + + + + + {'DocumentDir: ' + ReactNativeBlobUtil.fs.dirs.DocumentDir + '\n'} + {'CacheDir: ' + ReactNativeBlobUtil.fs.dirs.CacheDir + '\n'} + {'PictureDir: ' + ReactNativeBlobUtil.fs.dirs.PictureDir + '\n'} + {'MusicDir: ' + ReactNativeBlobUtil.fs.dirs.MusicDir + '\n'} + {'DownloadDir: ' + ReactNativeBlobUtil.fs.dirs.DownloadDir + '\n'} + {'DCIMDir: ' + ReactNativeBlobUtil.fs.dirs.DCIMDir + '\n'} + {'SDCardDir: ' + ReactNativeBlobUtil.fs.dirs.SDCardDir + '\n'} + {'SDCardApplicationDir: ' + ReactNativeBlobUtil.fs.dirs.SDCardApplicationDir + '\n'} + {'MainBundleDir: ' + ReactNativeBlobUtil.fs.dirs.MainBundleDir + '\n'} + {'LibraryDir: ' + ReactNativeBlobUtil.fs.dirs.LibraryDir + '\n'} + + + + + + + + {'exists - exists(), isDir()'} + setExistsParam(existsParam)} placeholderTextColor="#9a73ef" autoCapitalize="none" /> +